DAViCal
caldav-PUT-functions.php
1<?php
18require_once('AwlCache.php');
19require_once('vComponent.php');
20require_once('vCalendar.php');
21require_once('WritableCollection.php');
22require_once('schedule-functions.php');
23include_once('iSchedule.php');
24include_once('RRule.php');
25
26$bad_events = null;
27
31$GLOBALS['tz_regex'] = ':^(Africa|America|Antarctica|Arctic|Asia|Atlantic|Australia|Brazil|Canada|Chile|Etc|Europe|Indian|Mexico|Mideast|Pacific|US)/[a-z_]+$:i';
32
41function rollback_on_error( $caldav_context, $user_no, $path, $message='', $error_no=500 ) {
42 global $c, $bad_events;
43 if ( !$message ) $message = translate('Database error');
44 $qry = new AwlQuery();
45 if ( $qry->TransactionState() != 0 ) $qry->Rollback();
46 if ( $caldav_context ) {
47 if ( isset($bad_events) && isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import ) {
48 $bad_events[] = $message;
49 }
50 else {
51 global $request;
52 $request->DoResponse( $error_no, $message );
53 }
54 // and we don't return from that, ever...
55 }
56
57 $c->messages[] = sprintf(translate('Status: %d, Message: %s, User: %d, Path: %s'), $error_no, $message, $user_no, $path);
58
59}
60
61
62
72function controlRequestContainer( $username, $user_no, $path, $caldav_context, $public = null ) {
73 global $c, $request, $bad_events;
74
75 // Check to see if the path is like /foo /foo/bar or /foo/bar/baz etc. (not ending with a '/', but contains at least one)
76 if ( preg_match( '#^(.*/)([^/]+)$#', $path, $matches ) ) {//(
77 $request_container = $matches[1]; // get everything up to the last '/'
78 }
79 else {
80 // In this case we must have a URL with a trailing '/', so it must be a collection.
81 $request_container = $path;
82 }
83
84 if ( isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import ) {
85 $bad_events = array();
86 }
87
91 if ( $request_container == "/$username/" ) {
95 dbg_error_log( 'WARN', ' Storing events directly in user\'s base folders is not recommended!');
96 }
97 else {
98 $sql = 'SELECT * FROM collection WHERE dav_name = :dav_name';
99 $qry = new AwlQuery( $sql, array( ':dav_name' => $request_container) );
100 if ( ! $qry->Exec('PUT',__LINE__,__FILE__) ) {
101 rollback_on_error( $caldav_context, $user_no, $path, 'Database error in: '.$sql );
102 }
103 if ( !isset($c->readonly_webdav_collections) || $c->readonly_webdav_collections == true ) {
104 if ( $qry->rows() == 0 ) {
105 $request->DoResponse( 405 ); // Method not allowed
106 }
107 return;
108 }
109 if ( $qry->rows() == 0 ) {
110 if ( $public == true ) $public = 't'; else $public = 'f';
111 if ( preg_match( '{^(.*/)([^/]+)/$}', $request_container, $matches ) ) {
112 $parent_container = $matches[1];
113 $displayname = $matches[2];
114 }
115 $sql = 'INSERT INTO collection ( user_no, parent_container, dav_name, dav_etag, dav_displayname, is_calendar, created, modified, publicly_readable, resourcetypes )
116VALUES( :user_no, :parent_container, :dav_name, :dav_etag, :dav_displayname, TRUE, current_timestamp, current_timestamp, :is_public::boolean, :resourcetypes )';
117 $params = array(
118 ':user_no' => $user_no,
119 ':parent_container' => $parent_container,
120 ':dav_name' => $request_container,
121 ':dav_etag' => md5($user_no. $request_container),
122 ':dav_displayname' => $displayname,
123 ':is_public' => $public,
124 ':resourcetypes' => '<DAV::collection/><urn:ietf:params:xml:ns:caldav:calendar/>'
125 );
126 $qry->QDo( $sql, $params );
127 }
128 else if ( isset($public) ) {
129 $collection = $qry->Fetch();
130 if ( empty($collection->is_public) ) $collection->is_public = 'f';
131 if ( $collection->is_public == ($public?'t':'f') ) {
132 $sql = 'UPDATE collection SET publicly_readable = :is_public::boolean WHERE collection_id = :collection_id';
133 $params = array( ':is_public' => ($public?'t':'f'), ':collection_id' => $collection->collection_id );
134 if ( ! $qry->QDo($sql,$params) ) {
135 rollback_on_error( $caldav_context, $user_no, $path, 'Database error in: '.$sql );
136 }
137 }
138 }
139
140 }
141}
142
143
150function public_events_only( $user_no, $dav_name ) {
151 global $c;
152
153 $sql = 'SELECT public_events_only FROM collection WHERE dav_name = :dav_name';
154
155 $qry = new AwlQuery($sql, array(':dav_name' => $dav_name) );
156
157 if( $qry->Exec('PUT',__LINE__,__FILE__) && $qry->rows() == 1 ) {
158 $collection = $qry->Fetch();
159
160 if ($collection->public_events_only == 't') {
161 return true;
162 }
163 }
164
165 // Something went wrong, must be false.
166 return false;
167}
168
169
175function GetTZID( vComponent $comp ) {
176 $p = $comp->GetProperty('DTSTART');
177 if ( !isset($p) && $comp->GetType() == 'VTODO' ) {
178 $p = $comp->GetProperty('DUE');
179 }
180 if ( !isset($p) ) return null;
181 return $p->GetParameterValue('TZID');
182}
183
184
189function handle_schedule_request( $ical ) {
190 global $c, $session, $request;
191 $resources = $ical->GetComponents('VTIMEZONE',false);
192 $ic = $resources[0];
193 $etag = md5 ( $request->raw_post );
194 $reply = new XMLDocument( array("DAV:" => "", "urn:ietf:params:xml:ns:caldav" => "C" ) );
195 $responses = array();
196
197 $attendees = $ic->GetProperties('ATTENDEE');
198 $wr_attendees = $ic->GetProperties('X-WR-ATTENDEE');
199 if ( count ( $wr_attendees ) > 0 ) {
200 dbg_error_log( "PUT", "Non-compliant iCal request. Using X-WR-ATTENDEE property" );
201 foreach( $wr_attendees AS $k => $v ) {
202 $attendees[] = $v;
203 }
204 }
205 dbg_error_log( "PUT", "Attempting to deliver scheduling request for %d attendees", count($attendees) );
206
207 foreach( $attendees AS $k => $attendee ) {
208 $attendee_email = preg_replace( '/^mailto:/', '', $attendee->Value() );
209 if ( $attendee_email == $request->principal->email() ) {
210 dbg_error_log( "PUT", "not delivering to owner" );
211 continue;
212 }
213 $schedule_status = $attendee->GetParameterValue ( 'SCHEDULE-STATUS' );
214 if ( $attendee->GetParameterValue ( 'PARTSTAT' ) != 'NEEDS-ACTION' || (isset($schedule_status) && preg_match ( '/^[35]\.[3-9]/', $schedule_status ) ) ) {
215 dbg_error_log( "PUT", "attendee %s does not need action", $attendee_email );
216 continue;
217 }
218
219 if ( isset($c->enable_auto_schedule) && !$c->enable_auto_schedule ) {
220 // In this case we're being asked not to do auto-scheduling, so we build
221 // a response back for the client saying we can't...
222 $attendee->SetParameterValue ('SCHEDULE-STATUS','5.3;No scheduling support for user');
223 continue;
224 }
225
226 dbg_error_log( "PUT", "Delivering to %s", $attendee_email );
227
228 $attendee_principal = new DAVPrincipal ( array ('email'=>$attendee_email, 'options'=> array ( 'allow_by_email' => true ) ) );
229 if ( ! $attendee_principal->Exists() ){
230 $attendee->SetParameterValue ('SCHEDULE-STATUS','5.3;No scheduling support for user');
231 continue;
232 }
233 $deliver_path = $attendee_principal->internal_url('schedule-inbox');
234
235 $ar = new DAVResource($deliver_path);
236 $priv = $ar->HavePrivilegeTo('schedule-deliver-invite' );
237 if ( ! $ar->HavePrivilegeTo('schedule-deliver-invite' ) ){
238 $reply = new XMLDocument( array('DAV:' => '') );
239 $privnodes = array( $reply->href($attendee_principal->url('schedule-inbox')), new XMLElement( 'privilege' ) );
240 // RFC3744 specifies that we can only respond with one needed privilege, so we pick the first.
241 $reply->NSElement( $privnodes[1], 'schedule-deliver-invite' );
242 $xml = new XMLElement( 'need-privileges', new XMLElement( 'resource', $privnodes) );
243 $xmldoc = $reply->Render('error',$xml);
244 $request->DoResponse( 403, $xmldoc, 'text/xml; charset="utf-8"');
245 }
246
247
248 $attendee->SetParameterValue ('SCHEDULE-STATUS','1.2;Scheduling message has been delivered');
249 $ncal = new vCalendar( array('METHOD' => 'REQUEST') );
250 $ncal->AddComponent( array_merge( $ical->GetComponents('VEVENT',false), array($ic) ));
251 $content = $ncal->Render();
252 $cid = $ar->GetProperty('collection_id');
253 dbg_error_log('DELIVER', 'to user: %s, to path: %s, collection: %s, from user: %s, caldata %s', $attendee_principal->user_no(), $deliver_path, $cid, $request->user_no, $content );
254 $item_etag = md5($content);
255 write_resource( new DAVResource($deliver_path . $etag . '.ics'), $content, $ar, $request->user_no, $item_etag,
256 $put_action_type='INSERT', $caldav_context=true, $log_action=true, $etag );
257 $attendee->SetParameterValue ('SCHEDULE-STATUS','1.2;Scheduling message has been delivered');
258 }
259 // don't write an entry in the out box, ical doesn't delete it or ever read it again
260 $ncal = new vCalendar(array('METHOD' => 'REQUEST'));
261 $ncal->AddComponent ( array_merge ( $ical->GetComponents('VEVENT',false) , array ($ic) ));
262 $content = $ncal->Render();
263 $deliver_path = $request->principal->internal_url('schedule-inbox');
264 $ar = new DAVResource($deliver_path);
265 $item_etag = md5($content);
266 write_resource( new DAVResource($deliver_path . $etag . '.ics'), $content, $ar, $request->user_no, $item_etag,
267 $put_action_type='INSERT', $caldav_context=true, $log_action=true, $etag );
268 //$etag = md5($content);
269 header('ETag: "'. $etag . '"' );
270 header('Schedule-Tag: "'.$etag . '"' );
271 $request->DoResponse( 201, 'Created' );
272}
273
274
280function handle_schedule_reply ( vCalendar $ical ) {
281 global $c, $session, $request;
282 $resources = $ical->GetComponents('VTIMEZONE',false);
283 $ic = $resources[0];
284 $etag = md5 ( $request->raw_post );
285 $organizer = $ical->GetOrganizer();
286 $arrayOrganizer = array($organizer);
287 // for now we treat events with out organizers as an error
288 if ( empty( $arrayOrganizer ) ) return false;
289
290 $attendees = array_merge($arrayOrganizer,$ical->GetAttendees());
291 dbg_error_log( "PUT", "Attempting to deliver scheduling request for %d attendees", count($attendees) );
292
293 foreach( $attendees AS $k => $attendee ) {
294 $attendee_email = preg_replace( '/^mailto:/i', '', $attendee->Value() );
295 dbg_error_log( "PUT", "Delivering to %s", $attendee_email );
296 $attendee_principal = new DAVPrincipal ( array ('email'=>$attendee_email, 'options'=> array ( 'allow_by_email' => true ) ) );
297 $deliver_path = $attendee_principal->internal_url('schedule-inbox');
298 $attendee_email = preg_replace( '/^mailto:/i', '', $attendee->Value() );
299 if ( $attendee_email == $request->principal->email ) {
300 dbg_error_log( "PUT", "not delivering to owner" );
301 continue;
302 }
303 $ar = new DAVResource($deliver_path);
304 if ( ! $ar->HavePrivilegeTo('schedule-deliver-reply' ) ){
305 $reply = new XMLDocument( array('DAV:' => '') );
306 $privnodes = array( $reply->href($attendee_principal->url('schedule-inbox')), new XMLElement( 'privilege' ) );
307 // RFC3744 specifies that we can only respond with one needed privilege, so we pick the first.
308 $reply->NSElement( $privnodes[1], 'schedule-deliver-reply' );
309 $xml = new XMLElement( 'need-privileges', new XMLElement( 'resource', $privnodes) );
310 $xmldoc = $reply->Render('error',$xml);
311 $request->DoResponse( 403, $xmldoc, 'text/xml; charset="utf-8"' );
312 continue;
313 }
314
315 $ncal = new vCalendar( array('METHOD' => 'REPLY') );
316 $ncal->AddComponent ( array_merge ( $ical->GetComponents('VEVENT',false) , array ($ic) ));
317 $content = $ncal->Render();
318 write_resource( new DAVResource($deliver_path . $etag . '.ics'), $content, $ar, $request->user_no, md5($content),
319 $put_action_type='INSERT', $caldav_context=true, $log_action=true, $etag );
320 }
321 $request->DoResponse( 201, 'Created' );
322}
323
324
330function do_scheduling_reply( vCalendar $resource, vProperty $organizer ) {
331 global $request;
332 $organizer_email = preg_replace( '/^mailto:/i', '', $organizer->Value() );
333 $organizer_principal = new Principal('email',$organizer_email );
334 if ( !$organizer_principal->Exists() ) {
335 dbg_error_log( 'PUT', 'Organizer "%s" not found - cannot perform scheduling reply.', $organizer );
336 return false;
337 }
338 $sql = 'SELECT caldav_data.dav_name, caldav_data.caldav_data FROM caldav_data JOIN calendar_item USING(dav_id) ';
339 $sql .= 'WHERE caldav_data.collection_id IN (SELECT collection_id FROM collection WHERE is_calendar AND user_no =?) ';
340 $sql .= 'AND uid=? LIMIT 1';
341 $uids = $resource->GetPropertiesByPath('/VCALENDAR/*/UID');
342 if ( count($uids) == 0 ) {
343 dbg_error_log( 'PUT', 'No UID in VCALENDAR - giving up on REPLY.' );
344 return false;
345 }
346 $uid = $uids[0]->Value();
347 $qry = new AwlQuery($sql,$organizer_principal->user_no(), $uid);
348 if ( !$qry->Exec('PUT',__LINE__,__FILE__) || $qry->rows() < 1 ) {
349 dbg_error_log( 'PUT', 'Could not find original event from organizer - giving up on REPLY.' );
350 return false;
351 }
352 $row = $qry->Fetch();
353 $attendees = $resource->GetAttendees();
354 foreach( $attendees AS $v ) {
355 $email = preg_replace( '/^mailto:/i', '', $v->Value() );
356 if ( $email == $request->principal->email() ) {
357 $attendee = $v;
358 }
359 }
360 if ( empty($attendee) ) {
361 dbg_error_log( 'PUT', 'Could not find ATTENDEE in VEVENT - giving up on REPLY.' );
362 return false;
363 }
364 $schedule_original = new vCalendar($row->caldav_data);
365 $attendee->SetParameterValue('SCHEDULE-STATUS', '2.0');
366 $schedule_original->UpdateAttendeeStatus($request->principal->email(), clone($attendee) );
367
368 $collection_path = preg_replace('{/[^/]+$}', '/', $row->dav_name );
369 $segment_name = str_replace($collection_path, '', $row->dav_name );
370 $organizer_calendar = new WritableCollection(array('path' => $collection_path));
371 $organizer_inbox = new WritableCollection(array('path' => $organizer_principal->internal_url('schedule-inbox')));
372
373 $schedule_reply = GetItip(new vCalendar($schedule_original->Render(null, true)), 'REPLY', $attendee->Value(), array('CUTYPE'=>true, 'SCHEDULE-STATUS'=>true));
374
375 dbg_error_log( 'PUT', 'Writing scheduling REPLY from %s to %s', $request->principal->email(), $organizer_principal->email() );
376
377 $response = '3.7'; // Organizer was not found on server.
378 if ( !$organizer_calendar->Exists() ) {
379 dbg_error_log('ERROR','Default calendar at "%s" does not exist for user "%s"',
380 $organizer_calendar->dav_name(), $organizer_principal->username());
381 $response = '5.2'; // No scheduling support for user
382 }
383 else {
384 if ( ! $organizer_inbox->HavePrivilegeTo('schedule-deliver-reply') ) {
385 $response = '3.8'; // No authority to deliver replies to organizer.
386 }
387 else if ( $organizer_inbox->WriteCalendarMember($schedule_reply, false, false, $request->principal->username().$segment_name) !== false ) {
388 $response = '1.2'; // Scheduling reply delivered successfully
389 if ( $organizer_calendar->WriteCalendarMember($schedule_original, false, false, $segment_name) === false ) {
390 dbg_error_log('ERROR','Could not write updated calendar member to %s',
391 $organizer_calendar->dav_name());
392 trace_bug('Failed to write scheduling resource.');
393 }
394 }
395 }
396
397 $schedule_request = clone($schedule_original);
398 $schedule_request->AddProperty('METHOD', 'REQUEST');
399
400 dbg_error_log( 'PUT', 'Status for organizer <%s> set to "%s"', $organizer->Value(), $response );
401 $organizer->SetParameterValue( 'SCHEDULE-STATUS', $response );
402 $resource->UpdateOrganizerStatus($organizer);
403 $scheduling_actions = true;
404
405 $calling_attendee = clone($attendee);
406 $attendees = $schedule_original->GetAttendees();
407 foreach( $attendees AS $attendee ) {
408 $email = preg_replace( '/^mailto:/i', '', $attendee->Value() );
409 if ( $email == $request->principal->email() || $email == $organizer_principal->email() ) continue;
410
411 $agent = $attendee->GetParameterValue('SCHEDULE-AGENT');
412 if ( $agent && $agent != 'SERVER' ) {
413 dbg_error_log( "PUT", "not delivering to %s, schedule agent set to value other than server", $email );
414 continue;
415 }
416
417 // an attendee's reply should modify only the PARTSTAT on other attendees' objects
418 // other properties (that might have been adjusted individually by those other
419 // attendees) should remain unmodified. Therefore, we have to make $schedule_original
420 // and $schedule_request be initialized by each attendee's object here.
421 $attendee_principal = new DAVPrincipal ( array ('email'=>$email, 'options'=> array ( 'allow_by_email' => true ) ) );
422 if ( ! $attendee_principal->Exists() ){
423 dbg_error_log( 'PUT', 'Could not find attendee %s', $email);
424 continue;
425 }
426 $sql = 'SELECT caldav_data.dav_name, caldav_data.caldav_data, caldav_data.collection_id FROM caldav_data JOIN calendar_item USING(dav_id) ';
427 $sql .= 'WHERE caldav_data.collection_id IN (SELECT collection_id FROM collection WHERE is_calendar AND user_no =?) ';
428 $sql .= 'AND uid=? LIMIT 1';
429 $qry = new AwlQuery($sql,$attendee_principal->user_no(), $uid);
430 if ( !$qry->Exec('PUT',__LINE__,__FILE__) || $qry->rows() < 1 ) {
431 dbg_error_log( 'PUT', "Could not find attendee's event %s", $uid );
432 }
433 $row = $qry->Fetch();
434 $schedule_original = new vCalendar($row->caldav_data);
435 $schedule_original->UpdateAttendeeStatus($request->principal->email(), clone($calling_attendee) );
436 $schedule_request = clone($schedule_original);
437 $schedule_request->AddProperty('METHOD', 'REQUEST');
438
439 $schedule_target = new Principal('email',$email);
440 $response = '3.7'; // Attendee was not found on server.
441 if ( $schedule_target->Exists() ) {
442 // Instead of always writing to schedule-default-calendar, we first try to
443 // find a calendar with an existing instance of the event in any calendar of this attendee.
444 $r = new DAVResource($row);
445 $attendee_calendar = new WritableCollection(array('path' => $r->parent_path()));
446 if ($attendee_calendar->IsCalendar()) {
447 dbg_error_log( 'PUT', "found the event in attendee's calendar %s", $attendee_calendar->dav_name() );
448 } else {
449 dbg_error_log( 'PUT', 'could not find the event in any calendar, using schedule-default-calendar');
450 $attendee_calendar = new WritableCollection(array('path' => $schedule_target->internal_url('schedule-default-calendar')));
451 }
452 if ( !$attendee_calendar->Exists() ) {
453 dbg_error_log('ERROR','Default calendar at "%s" does not exist for user "%s"',
454 $attendee_calendar->dav_name(), $schedule_target->username());
455 $response = '5.2'; // No scheduling support for user
456 }
457 else {
458 $attendee_inbox = new WritableCollection(array('path' => $schedule_target->internal_url('schedule-inbox')));
459 if ( ! $attendee_inbox->HavePrivilegeTo('schedule-deliver-invite') ) {
460 $response = '3.8'; // No authority to deliver invitations to user.
461 }
462 else if ( $attendee_inbox->WriteCalendarMember($schedule_request, false) !== false ) {
463 $response = '1.2'; // Scheduling invitation delivered successfully
464 if ( $attendee_calendar->WriteCalendarMember($schedule_original, false) === false ) {
465 dbg_error_log('ERROR','Could not write updated calendar member to %s',
466 $attendee_calendar->dav_name(), $attendee_calendar->dav_name(), $schedule_target->username());
467 trace_bug('Failed to write scheduling resource.');
468 }
469 }
470 }
471 }
472 dbg_error_log( 'PUT', 'Status for attendee <%s> set to "%s"', $attendee->Value(), $response );
473 $attendee->SetParameterValue( 'SCHEDULE-STATUS', $response );
474 $scheduling_actions = true;
475
476 $resource->UpdateAttendeeStatus($email, clone($attendee));
477
478 }
479
480 return $scheduling_actions;
481}
482
483
491function do_scheduling_requests( vCalendar $resource, $create, $old_data = null ) {
492 global $request, $c;
493 if ( !isset($request) || (isset($c->enable_auto_schedule) && !$c->enable_auto_schedule) ) return false;
494
495 if ( ! is_object($resource) ) {
496 trace_bug( 'do_scheduling_requests called with non-object parameter (%s)', gettype($resource) );
497 return false;
498 }
499
500 $organizer = $resource->GetOrganizer();
501 if ( $organizer === false || empty($organizer) ) {
502 dbg_error_log( 'PUT', 'Event has no organizer - no scheduling required.' );
503 return false;
504 }
505 $organizer_email = preg_replace( '/^mailto:/i', '', $organizer->Value() );
506
507 // force re-render the object (to get the same representation for all attendiees)
508 $resource->Render(null, true);
509
510 if ( $request->principal->email() != $organizer_email ) {
511 return do_scheduling_reply($resource,$organizer);
512 }
513
514 if (isset($c->enable_attendee_group_resolution) && $c->enable_attendee_group_resolution) {
515 $mail_domain = preg_replace( '/^.*@/i', '', $c->admin_email );
516 $attendees = $resource->GetAttendees();
517 $new_attendees = array();
518 foreach( $attendees AS $attendee ) {
519 $v = $attendee->Value();
520 unset($localname);
521 if ($v == "invalid:nomail") {
522 $localname = $attendee->GetParameterValue("CN");
523 } else if ((preg_match('/^@/', $v) == 1) || (preg_match('/mailto:@/',$v) == 1)) {
524 $localname = preg_replace('/^.*@/', '', $v);
525 } else if (preg_match('/@/', $v) != 1) {
526 $localname = $v;
527 } else if (preg_match('/@'.$mail_domain.'/', $v) == 1) {
528 $localname = preg_replace('/@.*$/', '', $v);
529 $localname = preg_replace('/^mailto:/', '', $localname);
530 }
531 if ($localname) {
532 dbg_error_log( 'PUT', 'try to resolve local attendee %s', $localname);
533 $qry = new AwlQuery('SELECT fullname, email FROM usr WHERE user_no = (SELECT user_no FROM principal WHERE type_id = 1 AND user_no = (SELECT user_no FROM usr WHERE lower(username) = (text(:username)))) UNION SELECT fullname, email FROM usr WHERE user_no IN (SELECT user_no FROM principal WHERE principal_id IN (SELECT member_id FROM group_member WHERE group_id = (SELECT principal_id FROM principal WHERE type_id = 3 AND user_no = (SELECT user_no FROM usr WHERE lower(username) = (text(:username))))))', array(':username' => strtolower($localname)));
534 if ( $qry->Exec('PUT',__LINE__,__FILE__) && $qry->rows() >= 1 ) {
535 dbg_error_log( 'PUT', 'resolved local name %s to %d individual attendees', $localname, $qry->rows());
536 while ($row = $qry->Fetch()) {
537 if ($row->email == $request->principal->email()) continue;
538 dbg_error_log( 'PUT', 'adding individual attendee %s <%s>', $row->fullname, $row->email);
539 $a = clone($attendee);
540 $a->SetParameterValue("CN", $row->fullname);
541 $a->SetParameterValue("PARTSTAT", "NEEDS-ACTION");
542 $a->Value("mailto:" . $row->email);
543 $new_attendees[] = $a;
544 }
545 } else {
546 $new_attendees[] = clone($attendee);
547 }
548 } else {
549 $new_attendees[] = clone($attendee);
550 }
551 }
552 $events = $resource->GetComponents("VEVENT");
553 $event = $events[0];
554 $event->SetProperties($new_attendees,'ATTENDEE');
555 // this is just to reset the private attribute "attendees" in the $resource object
556 $resource->UpdateAttendeeStatus("this-is-nonsense", new vProperty("ATTENDEE:dummy"));
557 $attendees = $resource->GetAttendees();
558 }
559
560 // required because we set the schedule status on the original object (why clone() not works here?)
561 $orig_resource = new vCalendar($resource->Render(null, true));
562
563 $schedule_request = new vCalendar($resource->Render(null, true));
564 $schedule_request->AddProperty('METHOD', 'REQUEST');
565
566 $old_attendees = array();
567 if ( !empty($old_data) ) {
568 $old_resource = new vCalendar($old_data);
569 $old_attendees = $old_resource->GetAttendees();
570 }
571 $attendees = $resource->GetAttendees();
572 if ( count($attendees) == 0 && count($old_attendees) == 0 ) {
573 dbg_error_log( 'PUT', 'Event has no attendees - no scheduling required.', count($attendees) );
574 return false;
575 }
576 $removed_attendees = array();
577 foreach( $old_attendees AS $attendee ) {
578 $email = preg_replace( '/^mailto:/i', '', $attendee->Value() );
579 if ( $email == $request->principal->email() ) continue;
580 $removed_attendees[$email] = $attendee;
581 }
582
583 $uids = $resource->GetPropertiesByPath('/VCALENDAR/*/UID');
584 if ( count($uids) == 0 ) {
585 dbg_error_log( 'PUT', 'No UID in VCALENDAR - giving up on REPLY.' );
586 return false;
587 }
588 $uid = $uids[0]->Value();
589
590 dbg_error_log( 'PUT', 'Writing scheduling resources for %d attendees', count($attendees) );
591 $scheduling_actions = false;
592 foreach( $attendees AS $attendee ) {
593 $email = preg_replace( '/^mailto:/i', '', $attendee->Value() );
594 if ( $email == $request->principal->email() ) {
595 dbg_error_log( "PUT", "not delivering to owner '%s'", $request->principal->email() );
596 continue;
597 }
598
599 if ( $create ) {
600 $attendee_is_new = true;
601 }
602 else {
603 $attendee_is_new = !isset($removed_attendees[$email]);
604 if ( !$attendee_is_new ) unset($removed_attendees[$email]);
605 }
606
607 $agent = $attendee->GetParameterValue('SCHEDULE-AGENT');
608 if ( $agent && $agent != 'SERVER' ) {
609 dbg_error_log( "PUT", "not delivering to %s, schedule agent set to value other than server", $email );
610 continue;
611 }
612 $schedule_target = new Principal('email',$email);
613 $response = '3.7'; // Attendee was not found on server.
614 dbg_error_log( 'PUT', 'Handling scheduling resources for %s on %s which is %s', $email,
615 ($create?'create':'update'), ($attendee_is_new? 'new' : 'an update') );
616 if ( $schedule_target->Exists() ) {
617 // Instead of always writing to schedule-default-calendar, we first try to
618 // find a calendar with an existing instance of the event.
619 $sql = 'SELECT caldav_data.dav_name, caldav_data.caldav_data, caldav_data.collection_id FROM caldav_data JOIN calendar_item USING(dav_id) ';
620 $sql .= 'WHERE caldav_data.collection_id IN (SELECT collection_id FROM collection WHERE is_calendar AND user_no =?) ';
621 $sql .= 'AND uid=? LIMIT 1';
622 $qry = new AwlQuery($sql,$schedule_target->user_no(), $uid);
623 if ( !$qry->Exec('PUT',__LINE__,__FILE__) || $qry->rows() < 1 ) {
624 dbg_error_log( 'PUT', "Could not find event in attendee's calendars" );
625 $attendee_calendar = new WritableCollection(array('path' => $schedule_target->internal_url('schedule-default-calendar')));
626 unset($row); // signal "not found" below
627 } else {
628 $row = $qry->Fetch();
629 $r = new DAVResource($row);
630 $attendee_calendar = new WritableCollection(array('path' => $r->parent_path()));
631 if ($attendee_calendar->IsCalendar()) {
632 dbg_error_log( 'PUT', "found the event in attendee's calendar %s", $attendee_calendar->dav_name() );
633 } else {
634 dbg_error_log( 'PUT', 'could not find the event in any calendar, using schedule-default-calendar');
635 $attendee_calendar = new WritableCollection(array('path' => $schedule_target->internal_url('schedule-default-calendar')));
636 }
637 }
638 if ( !$attendee_calendar->Exists() ) {
639 dbg_error_log('ERROR','Default calendar at "%s" does not exist for user "%s"',
640 $attendee_calendar->dav_name(), $schedule_target->username());
641 $response = '5.2'; // No scheduling support for user
642 }
643 else {
644 if ($attendee_is_new || !isset($row)) {
645 $this_schedule_request = clone($schedule_request);
646 $this_resource = clone($resource);
647 } else {
648 dbg_error_log('PUT',"adjusting only some major properties in %s's instance of the event", $schedule_target->username());
649 $this_resource = new vCalendar($row->caldav_data);
650 $these_events = $this_resource->GetComponents("VEVENT");
651 $this_event = $these_events[0];
652 $events = $resource->GetComponents("VEVENT");
653 $event = $events[0];
654 // ??? CLASS, CREATED, LAST-MODIFIED, ATTENDEE, ORGANIZER, others???
655 $this_event->SetProperties( $event->GetProperties('DTSTAMP'), 'DTSTAMP' );
656 $this_event->SetProperties( $event->GetProperties('SEQUENCE'), 'SEQUENCE' );
657 $this_event->SetProperties( $event->GetProperties('DTSTART'), 'DTSTART' );
658 $this_event->SetProperties( $event->GetProperties('DTEND'), 'DTEND' );
659 $this_event->SetProperties( $event->GetProperties('DURATION'), 'DURATION' );
660 $this_event->SetProperties( $event->GetProperties('SUMMARY'), 'SUMMARY' );
661 $this_event->SetProperties( $event->GetProperties('LOCATION'), 'LOCATION' );
662 $this_event->SetProperties( $event->GetProperties('DESCRIPTION'), 'DESCRIPTION' );
663 $this_event->SetProperties( $event->GetProperties('GEO'), 'GEO' );
664 $this_event->SetProperties( $event->GetProperties('RESOURCES'), 'RESOURCES' );
665 $this_event->SetProperties( $event->GetProperties('STATUS'), 'STATUS' );
666 $this_event->SetProperties( $event->GetProperties('ATTENDEE'), 'ATTENDEE' );
667 $this_resource->SetComponents($these_events, "VEVENT");
668 $this_schedule_request = clone($this_resource);
669 $this_schedule_request->AddProperty('METHOD', 'REQUEST');
670 }
671 $attendee_inbox = new WritableCollection(array('path' => $schedule_target->internal_url('schedule-inbox')));
672 if ( ! $attendee_inbox->HavePrivilegeTo('schedule-deliver-invite') ) {
673 $response = '3.8'; // No authority to deliver invitations to user.
674 }
675 else if ( $attendee_inbox->WriteCalendarMember($this_schedule_request, $attendee_is_new) !== false ) {
676 $response = '1.2'; // Scheduling invitation delivered successfully
677 if ( $attendee_calendar->WriteCalendarMember($this_resource, $attendee_is_new) === false ) {
678 dbg_error_log('ERROR','Could not write %s calendar member to %s', ($attendee_is_new?'new':'updated'),
679 $attendee_calendar->dav_name(), $attendee_calendar->dav_name(), $schedule_target->username());
680 trace_bug('Failed to write scheduling resource.');
681 }
682 }
683 }
684 }
685 else {
686 $remote = new iSchedule ();
687 $answer = $remote->sendRequest ( $email, 'VEVENT/REQUEST', $schedule_request->Render() );
688 if ( $answer === false ) {
689 $response = '3.7'; // Invalid Calendar User: iSchedule request failed or DKIM not configured
690 }
691 else {
692 foreach ( $answer as $a ) // should only be one element in array
693 {
694 if ( $a === false ) {
695 $response = '3.7'; // Invalid Calendar User: weird reply from remote server
696 }
697 elseif ( substr( $a, 0, 1 ) >= 1 ) {
698 $response = $a; // NOTE: this may need to be limited to the reponse code
699 }
700 else {
701 $response = '2.0'; // Success
702 }
703 }
704 }
705 }
706 dbg_error_log( 'PUT', 'Status for attendee <%s> set to "%s"', $attendee->Value(), $response );
707 $attendee->SetParameterValue( 'SCHEDULE-STATUS', $response );
708 $scheduling_actions = true;
709 }
710
711 if ( !$create ) {
712 foreach( $removed_attendees AS $attendee ) {
713 $email = preg_replace( '/^mailto:/i', '', $attendee->Value() );
714 $schedule_target = new Principal('email',$email);
715 if ( $schedule_target->Exists() ) {
716 // Instead of always writing to schedule-default-calendar, we first try to
717 // find a calendar with an existing instance of the event.
718 $sql = 'SELECT caldav_data.dav_name, caldav_data.caldav_data, caldav_data.collection_id FROM caldav_data JOIN calendar_item USING(dav_id) ';
719 $sql .= 'WHERE caldav_data.collection_id IN (SELECT collection_id FROM collection WHERE is_calendar AND user_no =?) ';
720 $sql .= 'AND uid=? LIMIT 1';
721 $qry = new AwlQuery($sql,$schedule_target->user_no(), $uid);
722 if ( !$qry->Exec('PUT',__LINE__,__FILE__) || $qry->rows() < 1 ) {
723 dbg_error_log( 'PUT', "Could not find event in attendee's calendars" );
724 $attendee_calendar = new WritableCollection(array('path' => $schedule_target->internal_url('schedule-default-calendar')));
725 } else {
726 $row = $qry->Fetch();
727 $r = new DAVResource($row);
728 $attendee_calendar = new WritableCollection(array('path' => $r->parent_path()));
729 if ($attendee_calendar->IsCalendar()) {
730 dbg_error_log( 'PUT', "found the event in attendee's calendar %s", $attendee_calendar->dav_name() );
731 } else {
732 dbg_error_log( 'PUT', 'could not find the event in any calendar, using schedule-default-calendar');
733 $attendee_calendar = new WritableCollection(array('path' => $schedule_target->internal_url('schedule-default-calendar')));
734 }
735 dbg_error_log('PUT',"marking %s's instance of the event to show that the user's invitation has been revoked", $schedule_target->username());
736 $this_resource = new vCalendar($row->caldav_data);
737 $these_events = $this_resource->GetComponents("VEVENT");
738 $this_event = $these_events[0];
739 $properties[] = new vProperty( "DESCRIPTION:Your invitation to this event has been revoked." );
740 $this_event->SetProperties( $properties, 'DESCRIPTION' );
741 $properties[] = new vProperty( "STATUS:CANCELLED" );
742 $this_event->SetProperties( $properties, 'STATUS' );
743 $this_event->SetProperties( null, 'ATTENDEE' );
744 $this_resource->SetComponents($these_events, "VEVENT");
745 $this_schedule_request = clone($this_resource);
746 $this_schedule_request->AddProperty('METHOD', 'REQUEST');
747 $attendee_inbox = new WritableCollection(array('path' => $schedule_target->internal_url('schedule-inbox')));
748 if ( ! $attendee_inbox->HavePrivilegeTo('schedule-deliver-invite') ) {
749 $response = '3.8'; // No authority to deliver invitations to user.
750 }
751 else if ( $attendee_inbox->WriteCalendarMember($this_schedule_request, false) !== false ) {
752 $response = '1.2'; // Scheduling invitation delivered successfully
753 if ( $attendee_calendar->WriteCalendarMember($this_resource, false) === false ) {
754 dbg_error_log('ERROR','Could not write updated calendar member');
755 trace_bug('Failed to write scheduling resource.');
756 }
757 }
758 }
759 }
760 }
761 }
762 return $scheduling_actions;
763}
764
765
775function import_collection( $import_content, $user_no, $path, $caldav_context, $appending = false ) {
776 global $c;
777
778 if ( ! ini_get('open_basedir') && (isset($c->dbg['ALL']) || isset($c->dbg['put'])) ) {
779 $fh = fopen('/var/log/davical/PUT-2.debug','w');
780 if ( $fh ) {
781 fwrite($fh,$import_content);
782 fclose($fh);
783 }
784 }
785
786 if ( preg_match( '{^begin:(vcard|vcalendar)}i', $import_content, $matches) ) {
787 if ( strtoupper($matches[1]) == 'VCARD' )
788 import_addressbook_collection( $import_content, $user_no, $path, $caldav_context, $appending );
789 elseif ( strtoupper($matches[1]) == 'VCALENDAR' )
790 import_calendar_collection( $import_content, $user_no, $path, $caldav_context, $appending );
791
792 // Uncache anything to do with the collection
793 $cache = getCacheInstance();
794 $cache_ns = 'collection-'.preg_replace( '{/[^/]*$}', '/', $path);
795 $cache->delete( $cache_ns, null );
796 }
797 else {
798 dbg_error_log('PUT', 'Can only import files which are VCARD or VCALENDAR');
799 }
800}
801
802
810function import_addressbook_collection( $vcard_content, $user_no, $path, $caldav_context, $appending = false ) {
811 global $c, $session;
812 // We hack this into an enclosing component because vComponent only expects a single root component
813 $addressbook = new vComponent("BEGIN:ADDRESSES\r\n".$vcard_content."\r\nEND:ADDRESSES\r\n");
814
815 require_once('vcard.php');
816
817 $sql = 'SELECT * FROM collection WHERE dav_name = :dav_name';
818 $qry = new AwlQuery( $sql, array( ':dav_name' => $path) );
819 if ( ! $qry->Exec('PUT',__LINE__,__FILE__) ) rollback_on_error( $caldav_context, $user_no, $path, 'Database error in: '.$sql );
820 if ( ! $qry->rows() == 1 ) {
821 dbg_error_log( 'ERROR', ' PUT: Collection does not exist at "%s" for user %d', $path, $user_no );
822 rollback_on_error( $caldav_context, $user_no, $path, sprintf('Error: Collection does not exist at "%s" for user %d', $path, $user_no ));
823 }
824 $collection = $qry->Fetch();
825 $collection_id = $collection->collection_id;
826
827 // Fetch the current collection data
828 $qry->QDo('SELECT dav_name, caldav_data FROM caldav_data WHERE collection_id=:collection_id', array(
829 ':collection_id' => $collection_id
830 ));
831 $current_data = array();
832 while( $row = $qry->Fetch() )
833 $current_data[$row->dav_name] = $row->caldav_data;
834
835 if ( !(isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import) ) $qry->Begin();
836 $base_params = array(
837 ':collection_id' => $collection_id,
838 ':session_user' => $session->user_no,
839 ':caldav_type' => 'VCARD'
840 );
841
842 $dav_data_insert = <<<EOSQL
843INSERT INTO caldav_data ( user_no, dav_name, dav_etag, caldav_data, caldav_type, logged_user, created, modified, collection_id )
844 VALUES( :user_no, :dav_name, :etag, :dav_data, :caldav_type, :session_user, :created, :modified, :collection_id )
845EOSQL;
846
847 $dav_data_update = <<<EOSQL
848UPDATE caldav_data SET user_no=:user_no, caldav_data=:dav_data, dav_etag=:etag, caldav_type=:caldav_type, logged_user=:session_user,
849 modified=current_timestamp WHERE collection_id=:collection_id AND dav_name=:dav_name
850EOSQL;
851
852
853 $resources = $addressbook->GetComponents();
854 if ( count($resources) > 0 )
855 $qry->QDo('SELECT new_sync_token(0,'.$collection_id.')');
856
857 foreach( $resources AS $k => $resource ) {
858 if ( isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import ) $qry->Begin();
859
860 $vcard = new vCard( $resource->Render() );
861
862 $uid = $vcard->GetPValue('UID');
863 if ( empty($uid) ) {
864 $uid = uuid();
865 $vcard->AddProperty('UID',$uid);
866 }
867
868 $last_modified = $vcard->GetPValue('REV');
869 if ( empty($last_modified) ) {
870 $last_modified = gmdate( 'Ymd\THis\Z' );
871 $vcard->AddProperty('REV',$last_modified);
872 }
873
874 $created = $vcard->GetPValue('X-CREATED');
875 if ( empty($last_modified) ) {
876 $created = gmdate( 'Ymd\THis\Z' );
877 $vcard->AddProperty('X-CREATED',$created);
878 }
879
880 $rendered_card = $vcard->Render();
881
882 // We don't allow any of &?\/@%+: in the UID to appear in the path, but anything else is fair game.
883 $dav_name = sprintf( '%s%s.vcf', $path, preg_replace('{[&?\\/@%+:]}','',$uid) );
884
885 $dav_data_params = $base_params;
886 $dav_data_params[':user_no'] = $user_no;
887 $dav_data_params[':dav_name'] = $dav_name;
888 $dav_data_params[':etag'] = md5($rendered_card);
889 $dav_data_params[':dav_data'] = $rendered_card;
890 $dav_data_params[':modified'] = $last_modified;
891 $dav_data_params[':created'] = $created;
892
893 // Do we actually need to do anything?
894 $inserting = true;
895 if ( isset($current_data[$dav_name]) ) {
896 if ( $rendered_card == $current_data[$dav_name] ) {
897 unset($current_data[$dav_name]);
898 continue;
899 }
900 $sync_change = 200;
901 unset($current_data[$dav_name]);
902 $inserting = false;
903 }
904 else
905 $sync_change = 201;
906
907 if ( isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import ) $qry->Begin();
908
909 // Write to the caldav_data table
910 if ( !$qry->QDo( ($inserting ? $dav_data_insert : $dav_data_update), $dav_data_params) )
911 rollback_on_error( $caldav_context, $user_no, $path, 'Database error on:'. ($inserting ? $dav_data_insert : $dav_data_update));
912
913 // Get the dav_id for this row
914 $qry->QDo('SELECT dav_id FROM caldav_data WHERE dav_name = :dav_name ', array(':dav_name' => $dav_name));
915 if ( $qry->rows() == 1 && $row = $qry->Fetch() ) {
916 $dav_id = $row->dav_id;
917 }
918
919 $vcard->Write( $row->dav_id, !$inserting );
920
921 $qry->QDo("SELECT write_sync_change( $collection_id, $sync_change, :dav_name)", array(':dav_name' => $dav_name ) );
922
923 if ( isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import ) $qry->Commit();
924 }
925
926 if ( !$appending && count($current_data) > 0 ) {
927 $params = array( ':collection_id' => $collection_id );
928 if ( isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import ) $qry->Begin();
929 foreach( $current_data AS $dav_name => $data ) {
930 $params[':dav_name'] = $dav_name;
931 $qry->QDo('DELETE FROM caldav_data WHERE collection_id = :collection_id AND dav_name = :dav_name', $params);
932 $qry->QDo('SELECT write_sync_change(:collection_id, 404, :dav_name)', $params);
933 }
934 if ( isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import ) $qry->Commit();
935 }
936
937 if ( !(isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import) ) {
938 if ( ! $qry->Commit() ) rollback_on_error( $caldav_context, $user_no, $path, 'Database error on COMMIT');
939 }
940
941}
942
943
953function import_calendar_collection( $ics_content, $user_no, $path, $caldav_context, $appending = false ) {
954 global $c, $session, $tz_regex;
955 $calendar = new vComponent($ics_content);
956 $timezones = $calendar->GetComponents('VTIMEZONE',true);
957 $components = $calendar->GetComponents('VTIMEZONE',false);
958
959 // Add a parameter to calendars on import so it will only load events 'after' @author karora
960 // date, or an RFC5545 duration format offset from the current date.
961 $after = null;
962 if ( isset($_GET['after']) ) {
963 $after = $_GET['after'];
964 if ( strtoupper(substr($after, 0, 1)) == 'P' || strtoupper(substr($after, 0, 1)) == '-P' ) {
965 $duration = new Rfc5545Duration($after);
966 $duration = $duration->asSeconds();
967 $after = time() - (abs($duration));
968 }
969 else {
970 $after = new RepeatRuleDateTime($after);
971 $after = $after->epoch();
972 }
973 }
974
975 $displayname = $calendar->GetPValue('X-WR-CALNAME');
976 if ( !$appending && isset($displayname) ) {
977 $sql = 'UPDATE collection SET dav_displayname = :displayname WHERE dav_name = :dav_name';
978 $qry = new AwlQuery( $sql, array( ':displayname' => $displayname, ':dav_name' => $path) );
979 if ( ! $qry->Exec('PUT',__LINE__,__FILE__) ) rollback_on_error( $caldav_context, $user_no, $path, 'Database error on: '.$sql );
980 }
981
982
983 $tz_ids = array();
984 foreach( $timezones AS $k => $tz ) {
985 $tz_ids[$tz->GetPValue('TZID')] = $k;
986 }
987
989 $resources = array();
990 foreach( $components AS $k => $comp ) {
991 $uid = $comp->GetPValue('UID');
992 if ( $uid == null || $uid == '' ) {
993 $uid = uuid();
994 $comp->AddProperty('UID',$uid);
995 dbg_error_log( 'LOG WARN', ' PUT: New collection resource does not have a UID - we assign one!' );
996 }
997 if ( !isset($resources[$uid]) ) $resources[$uid] = array();
998 $resources[$uid][] = $comp;
999
1001 $tzid = GetTZID($comp);
1002 if ( !empty($tzid) && !isset($resources[$uid][$tzid]) && isset($tz_ids[$tzid]) ) {
1003 $resources[$uid][$tzid] = $timezones[$tz_ids[$tzid]];
1004 }
1005 }
1006
1007
1008 $sql = 'SELECT * FROM collection WHERE dav_name = :dav_name';
1009 $qry = new AwlQuery( $sql, array( ':dav_name' => $path) );
1010 if ( ! $qry->Exec('PUT',__LINE__,__FILE__) ) rollback_on_error( $caldav_context, $user_no, $path, 'Database error on: '.$sql );
1011 if ( ! $qry->rows() == 1 ) {
1012 dbg_error_log( 'ERROR', ' PUT: Collection does not exist at "%s" for user %d', $path, $user_no );
1013 rollback_on_error( $caldav_context, $user_no, $path, sprintf( 'Error: Collection does not exist at "%s" for user %d', $path, $user_no ));
1014 }
1015 $collection = $qry->Fetch();
1016 $collection_id = $collection->collection_id;
1017
1018 // Fetch the current collection data
1019 $qry->QDo('SELECT dav_name, caldav_data FROM caldav_data WHERE collection_id=:collection_id', array(
1020 ':collection_id' => $collection_id
1021 ));
1022 $current_data = array();
1023 while( $row = $qry->Fetch() )
1024 $current_data[$row->dav_name] = $row->caldav_data;
1025
1026 if ( !(isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import) ) $qry->Begin();
1027 $base_params = array( ':collection_id' => $collection_id );
1028
1029 $dav_data_insert = <<<EOSQL
1030INSERT INTO caldav_data ( user_no, dav_name, dav_etag, caldav_data, caldav_type, logged_user, created, modified, collection_id )
1031 VALUES( :user_no, :dav_name, :etag, :dav_data, :caldav_type, :session_user, current_timestamp, current_timestamp, :collection_id )
1032EOSQL;
1033
1034 $dav_data_update = <<<EOSQL
1035UPDATE caldav_data SET user_no=:user_no, caldav_data=:dav_data, dav_etag=:etag, caldav_type=:caldav_type, logged_user=:session_user,
1036 modified=current_timestamp WHERE collection_id=:collection_id AND dav_name=:dav_name
1037EOSQL;
1038
1039 $calitem_insert = <<<EOSQL
1040INSERT INTO calendar_item (user_no, dav_name, dav_id, dav_etag, uid, dtstamp,
1041 dtstart, dtstart_orig, dtend, dtend_orig, summary, location, class, transp,
1042 description, rrule, tz_id, last_modified, url, priority, created, due,
1043 percent_complete, status, collection_id )
1044VALUES ( :user_no, :dav_name, currval('dav_id_seq'), :etag, :uid, :dtstamp,
1045 :dtstart, :dtstart_orig, ##dtend##, :dtend_orig, :summary, :location,
1046 :class, :transp, :description, :rrule, :tzid, :modified, :url, :priority,
1047 :created, :due, :percent_complete, :status, :collection_id)
1048EOSQL;
1049
1050 $calitem_update = <<<EOSQL
1051UPDATE calendar_item
1052SET user_no=:user_no, dav_etag=:etag, uid=:uid, dtstamp=:dtstamp, dtstart=:dtstart,
1053 dtstart_orig=:dtstart_orig, dtend=##dtend##, summary=:summary,
1054 location=:location, class=:class, transp=:transp, description=:description,
1055 rrule=:rrule, tz_id=:tzid, last_modified=:modified, url=:url,
1056 priority=:priority, due=:due, percent_complete=:percent_complete,
1057 status=:status
1058WHERE collection_id=:collection_id AND dav_name=:dav_name
1059EOSQL;
1060
1061 $last_olson = '';
1062 if ( count($resources) > 0 )
1063 $qry->QDo('SELECT new_sync_token(0,'.$collection_id.')');
1064
1065 foreach( $resources AS $uid => $resource ) {
1066
1068 $vcal = new vCalendar();
1069 $vcal->SetComponents($resource);
1070 $icalendar = $vcal->Render();
1071 $dav_name = sprintf( '%s%s.ics', $path, preg_replace('{[&?\\/@%+:]}','',$uid) );
1072
1073 if ( isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import ) $qry->Begin();
1074
1076 $first = $resource[0];
1077
1078 $dav_data_params = $base_params;
1079 $dav_data_params[':user_no'] = $user_no;
1080 // We don't allow any of &?\/@%+: in the UID to appear in the path, but anything else is fair game.
1081 $dav_data_params[':dav_name'] = $dav_name;
1082 $dav_data_params[':etag'] = md5($icalendar);
1083 $calitem_params = $dav_data_params;
1084 $dav_data_params[':dav_data'] = $icalendar;
1085 $dav_data_params[':caldav_type'] = $first->GetType();
1086 $dav_data_params[':session_user'] = $session->user_no;
1087
1088 $dtstart = $first->GetPValue('DTSTART');
1089 $calitem_params[':dtstart_orig'] = $dtstart;
1090 $calitem_params[':dtstart'] = $dtstart;
1091
1092 if ( (!isset($dtstart) || $dtstart == '') && $first->GetPValue('DUE') != '' ) {
1093 $dtstart = $first->GetPValue('DUE');
1094 if ( isset($after) ) $dtstart_date = new RepeatRuleDateTime($first->GetProperty('DUE'));
1095 }
1096 else if ( isset($after) ) {
1097 $dtstart_date = new RepeatRuleDateTime($first->GetProperty('DTSTART'));
1098 }
1099
1100 $calitem_params[':rrule'] = $first->GetPValue('RRULE');
1101
1102 // Skip it if it's after our start date for this import.
1103 if ( isset($after) && empty($calitem_params[':rrule']) && $dtstart_date->epoch() < $after ) continue;
1104
1105 // Do we actually need to do anything?
1106 $inserting = true;
1107 if ( isset($current_data[$dav_name]) ) {
1108 if ( $icalendar == $current_data[$dav_name] ) {
1109 if ( $after == null ) {
1110 unset($current_data[$dav_name]);
1111 continue;
1112 }
1113 }
1114 $sync_change = 200;
1115 unset($current_data[$dav_name]);
1116 $inserting = false;
1117 }
1118 else
1119 $sync_change = 201;
1120
1121 // Write to the caldav_data table
1122 if ( !$qry->QDo( ($inserting ? $dav_data_insert : $dav_data_update), $dav_data_params) )
1123 rollback_on_error( $caldav_context, $user_no, $path, 'Database error on:'. ($inserting ? $dav_data_insert : $dav_data_update));
1124
1125 // Get the dav_id for this row
1126 $qry->QDo('SELECT dav_id FROM caldav_data WHERE dav_name = :dav_name ', array(':dav_name' => $dav_data_params[':dav_name']));
1127 if ( $qry->rows() == 1 && $row = $qry->Fetch() ) {
1128 $dav_id = $row->dav_id;
1129 }
1130
1131 $dtend = $first->GetPValue('DTEND');
1132 $calitem_params[':dtend_orig'] = $dtend;
1133
1134 if ( isset($dtend) && $dtend != '' ) {
1135 dbg_error_log( 'PUT', ' DTEND: "%s", DTSTART: "%s", DURATION: "%s"', $dtend, $dtstart, $first->GetPValue('DURATION') );
1136 $calitem_params[':dtend'] = $dtend;
1137 $dtend = ':dtend';
1138 }
1139 else {
1140 $dtend = 'NULL';
1141 if ( $first->GetPValue('DURATION') != '' AND $dtstart != '' ) {
1142 $duration = trim(preg_replace( '#[PT]#', ' ', $first->GetPValue('DURATION') ));
1143 if ( $duration == '' ) $duration = '0 seconds';
1144 $dtend = '(:dtstart::timestamp with time zone + :duration::interval)';
1145 $calitem_params[':duration'] = $duration;
1146 }
1147 elseif ( $first->GetType() == 'VEVENT' ) {
1161 $dtstart_prop = $first->GetProperty('DTSTART');
1162 if ( empty($dtstart_prop) ) {
1163 dbg_error_log('PUT','Invalid VEVENT without DTSTART, UID="%s" in collection %d', $uid, $collection_id);
1164 continue;
1165 }
1166 $value_type = $dtstart_prop->GetParameterValue('VALUE');
1167 dbg_error_log('PUT','DTSTART without DTEND. DTSTART value type is %s', $value_type );
1168 if ( isset($value_type) && $value_type == 'DATE' )
1169 $dtend = '(:dtstart::timestamp with time zone::date + \'1 day\'::interval)';
1170 else
1171 $dtend = ':dtstart';
1172 }
1173 }
1174
1175 $last_modified = $first->GetPValue('LAST-MODIFIED');
1176 if ( !isset($last_modified) || $last_modified == '' ) $last_modified = gmdate( 'Ymd\THis\Z' );
1177 $calitem_params[':modified'] = $last_modified;
1178
1179 $dtstamp = $first->GetPValue('DTSTAMP');
1180 if ( empty($dtstamp) ) $dtstamp = $last_modified;
1181 $calitem_params[':dtstamp'] = $dtstamp;
1182
1184 $class = ($collection->public_events_only == 't' && isset($class) ? 'PUBLIC' : $first->GetPValue('CLASS') );
1185 $calitem_params[':class'] = $class;
1186
1187
1189 $tzid = GetTZID($first);
1190 if ( !empty($tzid) && !empty($resource[$tzid]) ) {
1191 $tz = $resource[$tzid];
1192 $olson = $vcal->GetOlsonName($tz);
1193 dbg_error_log( 'PUT', ' Using TZID[%s] and location of [%s]', $tzid, (isset($olson) ? $olson : '') );
1194 if ( !empty($olson) && ($olson != $last_olson) && preg_match( $tz_regex, $olson ) ) {
1195 dbg_error_log( 'PUT', ' Setting timezone to %s', $olson );
1196 $qry->QDo('SET TIMEZONE TO \''.$olson."'" );
1197 $last_olson = $olson;
1198 }
1199 $params = array( ':tzid' => $tzid);
1200 $qry = new AwlQuery('SELECT 1 FROM timezones WHERE tzid = :tzid', $params );
1201 if ( $qry->Exec('PUT',__LINE__,__FILE__) && $qry->rows() == 0 ) {
1202 $params[':olson_name'] = $olson;
1203 $params[':vtimezone'] = (isset($tz) ? $tz->Render() : null );
1204 $params[':last_modified'] = (isset($tz) ? $tz->GetPValue('LAST-MODIFIED') : null );
1205 if ( empty($params[':last_modified']) ) {
1206 $params[':last_modified'] = gmdate('Ymd\THis\Z');
1207 }
1208 $qry->QDo('INSERT INTO timezones (tzid, olson_name, active, vtimezone, last_modified) VALUES(:tzid,:olson_name,false,:vtimezone,:last_modified)', $params );
1209 }
1210 }
1211 else {
1212 $tz = $olson = $tzid = null;
1213 }
1214
1215 $sql = str_replace( '##dtend##', $dtend, ($inserting ? $calitem_insert : $calitem_update) );
1216 $calitem_params[':uid'] = $first->GetPValue('UID');
1217 $calitem_params[':url'] = $first->GetPValue('URL');
1218 $calitem_params[':due'] = $first->GetPValue('DUE');
1219 $calitem_params[':tzid'] = $tzid;
1220 $calitem_params[':transp'] = $first->GetPValue('TRANSP');
1221 $calitem_params[':status'] = $first->GetPValue('STATUS');
1222 $calitem_params[':summary'] = $first->GetPValue('SUMMARY');
1223 $calitem_params[':location'] = $first->GetPValue('LOCATION');
1224 $calitem_params[':priority'] = $first->GetPValue('PRIORITY');
1225 $calitem_params[':description'] = $first->GetPValue('DESCRIPTION');
1226 $calitem_params[':percent_complete'] = $first->GetPValue('PERCENT-COMPLETE');
1227
1228 // Intentionally not populating first_instance_start and last_instance_end
1229 // here, As they may depend on the default tzid of the collection, which as
1230 // I understand it hasn't yet been set
1231
1232 if ( $inserting ) {
1233 $created = $first->GetPValue('CREATED');
1234 if ( $created == '00001231T000000Z' ) $created = '20001231T000000Z';
1235 $calitem_params[':created'] = $created;
1236 }
1237
1238 // Write the calendar_item row for this entry
1239 if ( !$qry->QDo($sql,$calitem_params) ) rollback_on_error( $caldav_context, $user_no, $path);
1240
1241 write_alarms($dav_id, $first);
1242 write_attendees($dav_id, $vcal);
1243
1244 $qry->QDo("SELECT write_sync_change( $collection_id, $sync_change, :dav_name)", array(':dav_name' => $dav_name ) );
1245
1246 do_scheduling_requests( $vcal, true );
1247 if ( isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import ) $qry->Commit();
1248 }
1249
1250 if ( !$appending && count($current_data) > 0 ) {
1251 $params = array( ':collection_id' => $collection_id );
1252 if ( isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import ) $qry->Begin();
1253 foreach( $current_data AS $dav_name => $data ) {
1254 $params[':dav_name'] = $dav_name;
1255 $qry->QDo('DELETE FROM caldav_data WHERE collection_id = :collection_id AND dav_name = :dav_name', $params);
1256 $qry->QDo('SELECT write_sync_change(:collection_id, 404, :dav_name)', $params);
1257 }
1258 if ( isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import ) $qry->Commit();
1259 }
1260
1261 if ( !(isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import) ) {
1262 if ( ! $qry->Commit() ) rollback_on_error( $caldav_context, $user_no, $path);
1263 }
1264}
1265
1266
1275function write_alarms( $dav_id, vComponent $ical ) {
1276 $qry = new AwlQuery('DELETE FROM calendar_alarm WHERE dav_id = '.$dav_id );
1277 $qry->Exec('PUT',__LINE__,__FILE__);
1278
1279 $alarms = $ical->GetComponents('VALARM');
1280 if ( count($alarms) < 1 ) return;
1281
1282 $qry->SetSql('INSERT INTO calendar_alarm ( dav_id, count, action, trigger, summary, description, component, next_trigger )
1283 VALUES( '.$dav_id.', :count, :action, :trigger, :summary, :description, :component,
1284 :related::timestamp with time zone + :related_trigger::interval )' );
1285 $qry->Prepare();
1286 $count = 0;
1287 foreach( $alarms AS $v ) {
1288 $trigger = array_merge($v->GetProperties('TRIGGER'));
1289 if ( $trigger == null ) continue; // Bogus data.
1290 $trigger = $trigger[0];
1291 $related = null;
1292 $related_trigger = '0M';
1293 $trigger_type = $trigger->GetParameterValue('VALUE');
1294 if ( !isset($trigger_type) || $trigger_type == 'DURATION' ) {
1295 switch ( $trigger->GetParameterValue('RELATED') ) {
1296 case 'DTEND': $related = $ical->GetProperty('DTEND'); break;
1297 case 'DUE': $related = $ical->GetProperty('DUE'); break;
1298 default: $related = $ical->GetProperty('DTSTART');
1299 }
1300 $duration = $trigger->Value();
1301 if ( !preg_match('{^-?P(:?\d+W)?(:?\d+D)?(:?T(:?\d+H)?(:?\d+M)?(:?\d+S)?)?$}', $duration ) ) continue;
1302 $minus = (substr($duration,0,1) == '-');
1303 $related_trigger = trim(preg_replace( '#[PT-]#', ' ', $duration ));
1304 if ($related_trigger == '') $related_trigger = '0 seconds';
1305 if ( $minus ) {
1306 $related_trigger = preg_replace( '{(\d+[WDHMS])}', '-$1 ', $related_trigger );
1307 }
1308 else {
1309 $related_trigger = preg_replace( '{(\d+[WDHMS])}', '$1 ', $related_trigger );
1310 }
1311 }
1312 else if ( $trigger_type == 'DATE-TIME' ) {
1313 $related = $trigger;
1314 }
1315 else {
1316 if ( false === strtotime($trigger->Value()) ) continue; // Invalid date.
1317 $related = $trigger;
1318 }
1319 $related_date = new RepeatRuleDateTime($related);
1320 $qry->Bind(':action', $v->GetPValue('ACTION'));
1321 $qry->Bind(':trigger', $trigger->Render());
1322 $qry->Bind(':summary', $v->GetPValue('SUMMARY'));
1323 $qry->Bind(':description', $v->GetPValue('DESCRIPTION'));
1324 $qry->Bind(':component', $v->Render());
1325 $qry->Bind(':related', $related_date->UTC() );
1326 $qry->Bind(':related_trigger', $related_trigger );
1327 $qry->Bind(':count', $count++ );
1328 $qry->Exec('PUT',__LINE__,__FILE__);
1329 }
1330}
1331
1332
1340function write_attendees( $dav_id, vCalendar $ical ) {
1341 $qry = new AwlQuery('DELETE FROM calendar_attendee WHERE dav_id = '.$dav_id );
1342 $qry->Exec('PUT',__LINE__,__FILE__);
1343
1344 $attendees = $ical->GetAttendees();
1345 if ( count($attendees) < 1 ) return;
1346
1347 $qry->SetSql('INSERT INTO calendar_attendee ( dav_id, status, partstat, cn, attendee, role, rsvp, property )
1348 VALUES( '.$dav_id.', :status, :partstat, :cn, :attendee, :role, :rsvp, :property )' );
1349 $qry->Prepare();
1350 $processed = array();
1351 foreach( $attendees AS $v ) {
1352 $attendee = $v->Value();
1353 if ( isset($processed[$attendee]) ) {
1354 dbg_error_log( 'LOG', 'Duplicate attendee "%s" in resource "%d"', $attendee, $dav_id );
1355 dbg_error_log( 'LOG', 'Original: "%s"', $processed[$attendee] );
1356 dbg_error_log( 'LOG', 'Duplicate: "%s"', $v->Render() );
1357 continue;
1358 }
1359 $qry->Bind(':attendee', $attendee );
1360 $qry->Bind(':status', $v->GetParameterValue('STATUS') );
1361 $qry->Bind(':partstat', $v->GetParameterValue('PARTSTAT') );
1362 $qry->Bind(':cn', $v->GetParameterValue('CN') );
1363 $qry->Bind(':role', $v->GetParameterValue('ROLE') );
1364 $qry->Bind(':rsvp', $v->GetParameterValue('RSVP') );
1365 $qry->Bind(':property', $v->Render() );
1366 $qry->Exec('PUT',__LINE__,__FILE__);
1367 $processed[$attendee] = $v->Render();
1368 }
1369}
1370
1371
1388function write_resource( DAVResource $resource, $caldav_data, DAVResource $collection, $author, &$etag, $put_action_type, $caldav_context, $log_action=true, $weak_etag=null ) {
1389 global $tz_regex, $session;
1390
1391 $path = $resource->bound_from();
1392 $user_no = $collection->user_no();
1393 $vcal = new vCalendar( $caldav_data );
1394 $resources = $vcal->GetComponents('VTIMEZONE',false); // Not matching VTIMEZONE
1395 if ( !isset($resources[0]) ) {
1396 $resource_type = 'Unknown';
1398 rollback_on_error( $caldav_context, $user_no, $path, translate('No calendar content'), 412 );
1399 return false;
1400 }
1401 else {
1402 $first = $resources[0];
1403 if ( !($first instanceof vComponent) ) {
1404 print $vcal->Render();
1405 fatal('This is not a vComponent!');
1406 }
1407 $resource_type = $first->GetType();
1408 }
1409
1410 $collection_id = $collection->collection_id();
1411
1412 $qry = new AwlQuery();
1413 $qry->Begin();
1414
1415 $calitem_params = array(
1416 ':etag' => $etag
1417 );
1418
1419 if ( $put_action_type == 'INSERT' ) {
1420 $qry->QDo('SELECT nextval(\'dav_id_seq\') AS dav_id, null AS caldav_data');
1421 }
1422 else {
1423 $qry->QDo('SELECT dav_id, caldav_data FROM caldav_data WHERE dav_name = :dav_name ', array(':dav_name' => $path));
1424 }
1425 if ( $qry->rows() != 1 || !($row = $qry->Fetch()) ) {
1426 // No dav_id? => We're toast!
1427 trace_bug( 'No dav_id for "%s" on %s!!!', $path, ($put_action_type == 'INSERT' ? 'create': 'update'));
1428 rollback_on_error( $caldav_context, $user_no, $path);
1429 return false;
1430 }
1431 $dav_id = $row->dav_id;
1432 $old_dav_data = $row->caldav_data;
1433
1434 // reset all attendees' partstat to needs-action in vcal and caldav_date here, if time of event has been modified.
1435 if ($old_dav_data) {
1436 $modified = false;
1437 $oldvcal = new vCalendar( $old_dav_data );
1438 $oldr = $oldvcal->GetComponents('VTIMEZONE',false);
1439 $oldfirst = $oldr[0];
1440 $dtstart = $first->GetPValue('DTSTART');
1441 $olddtstart = $oldfirst->GetPValue('DTSTART');
1442 if (strcmp($dtstart, $olddtstart)) $modified = true;
1443 $dtend = $first->GetPValue('DTEND');
1444 $olddtend = $oldfirst->GetPValue('DTEND');
1445 if (isset($dtend) && isset($olddtend) && strcmp($dtend, $olddtend)) $modified = true;
1446 $duration = $first->GetPValue('DURATION');
1447 $oldduration = $oldfirst->GetPValue('DURATION');
1448 $organizer = $vcal->GetOrganizer();
1449 if (isset($duration) && isset($oldduration) && strcmp($duration, $oldduration)) $modified = true;
1450 if (($modified == true) && !($organizer === false || empty($organizer))) {
1451 dbg_error_log( 'PUT', "event time attributes have been modified, reset all attendees' PARTSTAT to NEEDS-ACTION.");
1452 $organizer_email = preg_replace( '/^mailto:/i', '', $organizer->Value() );
1453 $attendees = $first->GetProperties('ATTENDEE');
1454 foreach( $attendees AS $attendee ) {
1455 $attendee_email = preg_replace( '/^mailto:/', '', $attendee->Value() );
1456 if ( $attendee_email != $organizer_email ) {
1457 $attendee->SetParameterValue("PARTSTAT", "NEEDS-ACTION");
1458 $attendee->SetParameterValue("SCHEDULE-STATUS", "1.0");
1459 }
1460 }
1461 $first->SetProperties($attendees,'ATTENDEE');
1462 $caldav_data = $vcal->Render(null, true);
1463 dbg_error_log( 'PUT', "event time attributes have been modified, reset all attendees' PARTSTAT completed.");
1464 }
1465 }
1466
1467 $dav_params = array(
1468 ':etag' => $etag,
1469 ':dav_data' => $caldav_data,
1470 ':caldav_type' => $resource_type,
1471 ':session_user' => $author,
1472 ':weak_etag' => $weak_etag
1473 );
1474
1475 $dav_params[':dav_id'] = $dav_id;
1476 $calitem_params[':dav_id'] = $dav_id;
1477
1478 $due = null;
1479 if ( $first->GetType() == 'VTODO' ) $due = $first->GetPValue('DUE');
1480 $calitem_params[':due'] = $due;
1481 $dtstart = $first->GetPValue('DTSTART');
1482 $calitem_params[':dtstart_orig'] = $dtstart;
1483
1484 if ( empty($dtstart) ) $dtstart = $due;
1485 if (isset($dtstart) && preg_match("/^1[0-8][0-9][0-9][01][0-9][0-3][0-9]$/", $dtstart))
1486 $dtstart = $dtstart . "T000000Z";
1487 $calitem_params[':dtstart'] = $dtstart;
1488
1489 $dtend = $first->GetPValue('DTEND');
1490 $calitem_params[':dtend_orig'] = $dtend;
1491
1492 if ( isset($dtend) && $dtend != '' ) {
1493 dbg_error_log( 'PUT', ' DTEND: "%s", DTSTART: "%s", DURATION: "%s"', $dtend, $dtstart, $first->GetPValue('DURATION') );
1494 if (preg_match("/^1[0-8][0-9][0-9][01][0-9][0-3][0-9]$/", $dtend))
1495 $dtend = $dtend . "T000000Z";
1496 $calitem_params[':dtend'] = $dtend;
1497 $dtend = ':dtend';
1498 }
1499 else {
1500 // In this case we'll construct the SQL directly as a calculation relative to :dtstart
1501 $dtend = 'NULL';
1502 if ( $first->GetPValue('DURATION') != '' AND $dtstart != '' ) {
1503 $duration = trim(preg_replace( '#[PT]#', ' ', $first->GetPValue('DURATION') ));
1504 if ( $duration == '' ) $duration = '0 seconds';
1505 $dtend = '(:dtstart::timestamp with time zone + :duration::interval)';
1506 $calitem_params[':duration'] = $duration;
1507 }
1508 elseif ( $first->GetType() == 'VEVENT' ) {
1522 $dtstart_prop = $first->GetProperty('DTSTART');
1523 $value_type = $dtstart_prop->GetParameterValue('VALUE');
1524 dbg_error_log('PUT','DTSTART without DTEND. DTSTART value type is %s', $value_type );
1525 if ( isset($value_type) && $value_type == 'DATE' )
1526 $dtend = '(:dtstart::timestamp with time zone::date + \'1 day\'::interval)';
1527 else
1528 $dtend = ':dtstart';
1529 }
1530 }
1531
1532 $dtstamp = $first->GetPValue('DTSTAMP');
1533 if ( !isset($dtstamp) || $dtstamp == '' ) {
1534 // Strictly, we're dealing with an out of spec component here, but we'll try and survive
1535 $dtstamp = gmdate( 'Ymd\THis\Z' );
1536 }
1537 $calitem_params[':dtstamp'] = $dtstamp;
1538
1539 $last_modified = $first->GetPValue('LAST-MODIFIED');
1540 if ( !isset($last_modified) || $last_modified == '' ) $last_modified = $dtstamp;
1541 $dav_params[':modified'] = $last_modified;
1542 $calitem_params[':modified'] = $last_modified;
1543
1544 $created = $first->GetPValue('CREATED');
1545 if ( $created == '00001231T000000Z' ) $created = '20001231T000000Z';
1546
1547 $class = $first->GetPValue('CLASS');
1548 /* Check and see if we should over ride the class. */
1550 if ( public_events_only($user_no, $path) && isset($class) ) {
1551 $class = 'PUBLIC';
1552 }
1553
1554 $calitem_params[':class'] = $class;
1555
1557 $last_olson = 'Turkmenikikamukau'; // I really hope this location doesn't exist!
1558 $tzid = GetTZID($first);
1559 if ( !empty($tzid) ) {
1560 $timezones = $vcal->GetComponents('VTIMEZONE');
1561 foreach( $timezones AS $k => $tz ) {
1562 if ( $tz->GetPValue('TZID') != $tzid ) {
1566 dbg_error_log( 'ERROR', ' Event uses TZID[%s], skipping included TZID[%s]!', $tz->GetPValue('TZID'), $tzid );
1567 continue;
1568 }
1569 $olson = olson_from_tzstring($tzid);
1570 if ( empty($olson) ) {
1571 $olson = $tz->GetPValue('X-LIC-LOCATION');
1572 if ( !empty($olson) ) {
1573 $olson = olson_from_tzstring($olson);
1574 }
1575 }
1576 }
1577
1578 dbg_error_log( 'PUT', ' Using TZID[%s] and location of [%s]', $tzid, (isset($olson) ? $olson : '') );
1579 if ( !empty($olson) && ($olson != $last_olson) && preg_match( $tz_regex, $olson ) ) {
1580 dbg_error_log( 'PUT', ' Setting timezone to %s', $olson );
1581 if ( $olson != '' ) {
1582 $qry->QDo('SET TIMEZONE TO \''.$olson."'" );
1583 }
1584 $last_olson = $olson;
1585 }
1586 $params = array( ':tzid' => $tzid);
1587 $qry = new AwlQuery('SELECT 1 FROM timezones WHERE tzid = :tzid', $params );
1588 if ( $qry->Exec('PUT',__LINE__,__FILE__) && $qry->rows() == 0 ) {
1589 $params[':olson_name'] = $olson;
1590 $params[':vtimezone'] = (isset($tz) ? $tz->Render() : null );
1591 $qry->QDo('INSERT INTO timezones (tzid, olson_name, active, vtimezone) VALUES(:tzid,:olson_name,false,:vtimezone)', $params );
1592 }
1593 if ( !isset($olson) || $olson == '' ) $olson = $tzid;
1594
1595 }
1596
1597 $qry->QDo('SELECT new_sync_token(0,'.$collection_id.')');
1598
1599 $calitem_params[':tzid'] = $tzid;
1600 $calitem_params[':uid'] = $first->GetPValue('UID');
1601 $calitem_params[':summary'] = $first->GetPValue('SUMMARY');
1602 $calitem_params[':location'] = $first->GetPValue('LOCATION');
1603 $calitem_params[':transp'] = $first->GetPValue('TRANSP');
1604 $calitem_params[':description'] = $first->GetPValue('DESCRIPTION');
1605 $calitem_params[':rrule'] = $first->GetPValue('RRULE');
1606 $calitem_params[':url'] = $first->GetPValue('URL');
1607 $calitem_params[':priority'] = $first->GetPValue('PRIORITY');
1608 $calitem_params[':percent_complete'] = $first->GetPValue('PERCENT-COMPLETE');
1609 $calitem_params[':status'] = $first->GetPValue('STATUS');
1610
1611 $range = getVCalendarRange($vcal, $resource->timezone_name());
1612 $calitem_params[':first_instance_start'] = isset($range->from) ? $range->from->UTC() : null;
1613 $calitem_params[':last_instance_end'] = isset($range->until) ? $range->until->UTC() : null;
1614
1615 // force re-render the object (to get the same representation for all attendees)
1616 $vcal->Render(null, true);
1617
1618 if ( !$collection->IsSchedulingCollection() ) {
1619 if ( do_scheduling_requests($vcal, ($put_action_type == 'INSERT'), $old_dav_data ) ) {
1620 $dav_params[':dav_data'] = $vcal->Render(null, true);
1621 $etag = null;
1622 }
1623 }
1624
1625 if ( !isset($dav_params[':modified']) ) $dav_params[':modified'] = 'now';
1626 if ( $put_action_type == 'INSERT' ) {
1627 $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 )
1628 VALUES( :dav_id, :user_no, :dav_name, :etag, :dav_data, :caldav_type, :session_user, :created, :modified, :collection_id, :weak_etag )';
1629 $dav_params[':collection_id'] = $collection_id;
1630 $dav_params[':user_no'] = $user_no;
1631 $dav_params[':dav_name'] = $path;
1632 $dav_params[':created'] = (isset($created) && $created != '' ? $created : $dtstamp);
1633 }
1634 else {
1635 $sql = 'UPDATE caldav_data SET caldav_data=:dav_data, dav_etag=:etag, caldav_type=:caldav_type, logged_user=:session_user,
1636 modified=:modified, weak_etag=:weak_etag WHERE dav_id=:dav_id';
1637 }
1638 $qry = new AwlQuery($sql,$dav_params);
1639 if ( !$qry->Exec('PUT',__LINE__,__FILE__) ) {
1640 fatal('Insert into calendar_item failed...');
1641 rollback_on_error( $caldav_context, $user_no, $path);
1642 return false;
1643 }
1644
1645
1646 if ( $put_action_type == 'INSERT' ) {
1647 $sql = <<<EOSQL
1648INSERT INTO calendar_item (user_no, dav_name, dav_id, dav_etag, uid, dtstamp,
1649 dtstart, dtstart_orig, dtend, dtend_orig, summary, location, class, transp,
1650 description, rrule, tz_id, last_modified, url, priority, created, due,
1651 percent_complete, status, collection_id, first_instance_start,
1652 last_instance_end )
1653VALUES ( :user_no, :dav_name, :dav_id, :etag, :uid, :dtstamp, :dtstart,
1654 :dtstart_orig, $dtend, :dtend_orig, :summary, :location, :class, :transp,
1655 :description, :rrule, :tzid, :modified, :url, :priority, :created, :due,
1656 :percent_complete, :status, :collection_id, :first_instance_start,
1657 :last_instance_end)
1658EOSQL;
1659 $sync_change = 201;
1660 $calitem_params[':collection_id'] = $collection_id;
1661 $calitem_params[':user_no'] = $user_no;
1662 $calitem_params[':dav_name'] = $path;
1663 $calitem_params[':created'] = $dav_params[':created'];
1664 }
1665 else {
1666 $sql = <<<EOSQL
1667UPDATE calendar_item SET dav_etag=:etag, uid=:uid, dtstamp=:dtstamp,
1668 dtstart=:dtstart, dtstart_orig=:dtstart_orig, dtend=$dtend,
1669 dtend_orig=:dtend_orig, summary=:summary, location=:location, class=:class,
1670 transp=:transp, description=:description, rrule=:rrule, tz_id=:tzid,
1671 last_modified=:modified, url=:url, priority=:priority, due=:due,
1672 percent_complete=:percent_complete, status=:status,
1673 first_instance_start=:first_instance_start,
1674 last_instance_end=:last_instance_end
1675WHERE dav_id=:dav_id
1676EOSQL;
1677 $sync_change = 200;
1678 }
1679
1680 write_alarms($dav_id, $first);
1681 write_attendees($dav_id, $vcal);
1682
1683 if ( $log_action && function_exists('log_caldav_action') ) {
1684 log_caldav_action( $put_action_type, $first->GetPValue('UID'), $user_no, $collection_id, $path );
1685 }
1686 else if ( $log_action ) {
1687 dbg_error_log( 'PUT', 'No log_caldav_action( %s, %s, %s, %s, %s) can be called.',
1688 $put_action_type, $first->GetPValue('UID'), $user_no, $collection_id, $path );
1689 }
1690
1691 $qry = new AwlQuery( $sql, $calitem_params );
1692 if ( !$qry->Exec('PUT',__LINE__,__FILE__) ) {
1693 rollback_on_error( $caldav_context, $user_no, $path);
1694 return false;
1695 }
1696 $qry->QDo("SELECT write_sync_change( :collection_id, $sync_change, :dav_name)",
1697 array(':collection_id' => $collection_id, ':dav_name' => $path ) );
1698 $qry->Commit();
1699
1700 if ( function_exists('post_commit_action') ) {
1701 post_commit_action( $put_action_type, $first->GetPValue('UID'), $user_no, $collection_id, $path );
1702 }
1703
1704 // Uncache anything to do with the collection
1705 $cache = getCacheInstance();
1706 $cache_ns = 'collection-'.preg_replace( '{/[^/]*$}', '/', $path);
1707 $cache->delete( $cache_ns, null );
1708
1709 dbg_error_log( 'PUT', 'User: %d, ETag: %s, Path: %s', $author, $etag, $path);
1710
1711 return true; // Success!
1712}
1713
1714
1715
1725function simple_write_resource( $path, $caldav_data, $put_action_type, $write_action_log = false ) {
1726 global $session;
1727
1731 $dav_resource = new DAVResource($path);
1732 $etag = md5($caldav_data);
1733 $collection_path = preg_replace( '#/[^/]*$#', '/', $path );
1734 $collection = new DAVResource($collection_path);
1735 if ( $collection->IsCollection() || $collection->IsSchedulingCollection() ) {
1736 return write_resource( $dav_resource, $caldav_data, $collection, $session->user_no, $etag, $put_action_type, false, $write_action_log );
1737 }
1738 return false;
1739}