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');
31$GLOBALS[
'tz_regex'] =
':^(Africa|America|Antarctica|Arctic|Asia|Atlantic|Australia|Brazil|Canada|Chile|Etc|Europe|Indian|Mexico|Mideast|Pacific|US)/[a-z_]+$:i';
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;
52 $request->DoResponse( $error_no, $message );
57 $c->messages[] = sprintf(translate(
'Status: %d, Message: %s, User: %d, Path: %s'), $error_no, $message, $user_no, $path);
72function controlRequestContainer( $username, $user_no, $path, $caldav_context, $public =
null ) {
73 global $c, $request, $bad_events;
76 if ( preg_match(
'#^(.*/)([^/]+)$#', $path, $matches ) ) {
77 $request_container = $matches[1];
81 $request_container = $path;
84 if ( isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import ) {
85 $bad_events = array();
91 if ( $request_container ==
"/$username/" ) {
95 dbg_error_log(
'WARN',
' Storing events directly in user\'s base folders is not recommended!');
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 );
103 if ( !isset($c->readonly_webdav_collections) || $c->readonly_webdav_collections ==
true ) {
104 if ( $qry->rows() == 0 ) {
105 $request->DoResponse( 405 );
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];
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 )';
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/>'
126 $qry->QDo( $sql, $params );
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 );
150function public_events_only( $user_no, $dav_name ) {
153 $sql =
'SELECT public_events_only FROM collection WHERE dav_name = :dav_name';
155 $qry =
new AwlQuery($sql, array(
':dav_name' => $dav_name) );
157 if( $qry->Exec(
'PUT',__LINE__,__FILE__) && $qry->rows() == 1 ) {
158 $collection = $qry->Fetch();
160 if ($collection->public_events_only ==
't') {
175function GetTZID( vComponent $comp ) {
176 $p = $comp->GetProperty(
'DTSTART');
177 if ( !isset($p) && $comp->GetType() ==
'VTODO' ) {
178 $p = $comp->GetProperty(
'DUE');
180 if ( !isset($p) )
return null;
181 return $p->GetParameterValue(
'TZID');
189function handle_schedule_request( $ical ) {
190 global $c, $session, $request;
191 $resources = $ical->GetComponents(
'VTIMEZONE',
false);
193 $etag = md5 ( $request->raw_post );
194 $reply =
new XMLDocument( array(
"DAV:" =>
"",
"urn:ietf:params:xml:ns:caldav" =>
"C" ) );
195 $responses = array();
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 ) {
205 dbg_error_log(
"PUT",
"Attempting to deliver scheduling request for %d attendees", count($attendees) );
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" );
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 );
219 if ( isset($c->enable_auto_schedule) && !$c->enable_auto_schedule ) {
222 $attendee->SetParameterValue (
'SCHEDULE-STATUS',
'5.3;No scheduling support for user');
226 dbg_error_log(
"PUT",
"Delivering to %s", $attendee_email );
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');
233 $deliver_path = $attendee_principal->internal_url(
'schedule-inbox');
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' ) );
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"');
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');
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');
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 );
269 header(
'ETag: "'. $etag .
'"' );
270 header(
'Schedule-Tag: "'.$etag .
'"' );
271 $request->DoResponse( 201,
'Created' );
280function handle_schedule_reply ( vCalendar $ical ) {
281 global $c, $session, $request;
282 $resources = $ical->GetComponents(
'VTIMEZONE',
false);
284 $etag = md5 ( $request->raw_post );
285 $organizer = $ical->GetOrganizer();
286 $arrayOrganizer = array($organizer);
288 if ( empty( $arrayOrganizer ) )
return false;
290 $attendees = array_merge($arrayOrganizer,$ical->GetAttendees());
291 dbg_error_log(
"PUT",
"Attempting to deliver scheduling request for %d attendees", count($attendees) );
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" );
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' ) );
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"' );
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 );
321 $request->DoResponse( 201,
'Created' );
330function do_scheduling_reply( vCalendar $resource, vProperty $organizer ) {
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 );
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.' );
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.' );
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() ) {
360 if ( empty($attendee) ) {
361 dbg_error_log(
'PUT',
'Could not find ATTENDEE in VEVENT - giving up on REPLY.' );
364 $schedule_original =
new vCalendar($row->caldav_data);
365 $attendee->SetParameterValue(
'SCHEDULE-STATUS',
'2.0');
366 $schedule_original->UpdateAttendeeStatus($request->principal->email(), clone($attendee) );
368 $collection_path = preg_replace(
'{/[^/]+$}',
'/', $row->dav_name );
369 $segment_name = str_replace($collection_path,
'', $row->dav_name );
371 $organizer_inbox =
new WritableCollection(array(
'path' => $organizer_principal->internal_url(
'schedule-inbox')));
373 $schedule_reply = GetItip(
new vCalendar($schedule_original->Render(
null,
true)),
'REPLY', $attendee->Value(), array(
'CUTYPE'=>
true,
'SCHEDULE-STATUS'=>
true));
375 dbg_error_log(
'PUT',
'Writing scheduling REPLY from %s to %s', $request->principal->email(), $organizer_principal->email() );
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());
384 if ( ! $organizer_inbox->HavePrivilegeTo(
'schedule-deliver-reply') ) {
387 else if ( $organizer_inbox->WriteCalendarMember($schedule_reply,
false,
false, $request->principal->username().$segment_name) !==
false ) {
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.');
397 $schedule_request = clone($schedule_original);
398 $schedule_request->AddProperty(
'METHOD',
'REQUEST');
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;
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;
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 );
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);
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 );
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');
439 $schedule_target =
new Principal(
'email',$email);
441 if ( $schedule_target->Exists() ) {
446 if ($attendee_calendar->IsCalendar()) {
447 dbg_error_log(
'PUT',
"found the event in attendee's calendar %s", $attendee_calendar->dav_name() );
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')));
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());
458 $attendee_inbox =
new WritableCollection(array(
'path' => $schedule_target->internal_url(
'schedule-inbox')));
459 if ( ! $attendee_inbox->HavePrivilegeTo(
'schedule-deliver-invite') ) {
462 else if ( $attendee_inbox->WriteCalendarMember($schedule_request,
false) !==
false ) {
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.');
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;
476 $resource->UpdateAttendeeStatus($email, clone($attendee));
480 return $scheduling_actions;
491function do_scheduling_requests( vCalendar $resource, $create, $old_data =
null ) {
493 if ( !isset($request) || (isset($c->enable_auto_schedule) && !$c->enable_auto_schedule) )
return false;
495 if ( ! is_object($resource) ) {
496 trace_bug(
'do_scheduling_requests called with non-object parameter (%s)', gettype($resource) );
500 $organizer = $resource->GetOrganizer();
501 if ( $organizer ===
false || empty($organizer) ) {
502 dbg_error_log(
'PUT',
'Event has no organizer - no scheduling required.' );
505 $organizer_email = preg_replace(
'/^mailto:/i',
'', $organizer->Value() );
508 $resource->Render(
null,
true);
510 if ( $request->principal->email() != $organizer_email ) {
511 return do_scheduling_reply($resource,$organizer);
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();
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) {
527 }
else if (preg_match(
'/@'.$mail_domain.
'/', $v) == 1) {
528 $localname = preg_replace(
'/@.*$/',
'', $v);
529 $localname = preg_replace(
'/^mailto:/',
'', $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;
546 $new_attendees[] = clone($attendee);
549 $new_attendees[] = clone($attendee);
552 $events = $resource->GetComponents(
"VEVENT");
554 $event->SetProperties($new_attendees,
'ATTENDEE');
556 $resource->UpdateAttendeeStatus(
"this-is-nonsense",
new vProperty(
"ATTENDEE:dummy"));
557 $attendees = $resource->GetAttendees();
561 $orig_resource =
new vCalendar($resource->Render(
null,
true));
563 $schedule_request =
new vCalendar($resource->Render(
null,
true));
564 $schedule_request->AddProperty(
'METHOD',
'REQUEST');
566 $old_attendees = array();
567 if ( !empty($old_data) ) {
568 $old_resource =
new vCalendar($old_data);
569 $old_attendees = $old_resource->GetAttendees();
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) );
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;
583 $uids = $resource->GetPropertiesByPath(
'/VCALENDAR/*/UID');
584 if ( count($uids) == 0 ) {
585 dbg_error_log(
'PUT',
'No UID in VCALENDAR - giving up on REPLY.' );
588 $uid = $uids[0]->Value();
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() );
600 $attendee_is_new =
true;
603 $attendee_is_new = !isset($removed_attendees[$email]);
604 if ( !$attendee_is_new ) unset($removed_attendees[$email]);
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 );
612 $schedule_target =
new Principal(
'email',$email);
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() ) {
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')));
628 $row = $qry->Fetch();
631 if ($attendee_calendar->IsCalendar()) {
632 dbg_error_log(
'PUT',
"found the event in attendee's calendar %s", $attendee_calendar->dav_name() );
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')));
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());
644 if ($attendee_is_new || !isset($row)) {
645 $this_schedule_request = clone($schedule_request);
646 $this_resource = clone($resource);
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");
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');
671 $attendee_inbox =
new WritableCollection(array(
'path' => $schedule_target->internal_url(
'schedule-inbox')));
672 if ( ! $attendee_inbox->HavePrivilegeTo(
'schedule-deliver-invite') ) {
675 else if ( $attendee_inbox->WriteCalendarMember($this_schedule_request, $attendee_is_new) !==
false ) {
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.');
687 $answer = $remote->sendRequest ( $email,
'VEVENT/REQUEST', $schedule_request->Render() );
688 if ( $answer ===
false ) {
692 foreach ( $answer as $a )
694 if ( $a ===
false ) {
697 elseif ( substr( $a, 0, 1 ) >= 1 ) {
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;
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() ) {
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')));
726 $row = $qry->Fetch();
729 if ($attendee_calendar->IsCalendar()) {
730 dbg_error_log(
'PUT',
"found the event in attendee's calendar %s", $attendee_calendar->dav_name() );
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')));
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') ) {
751 else if ( $attendee_inbox->WriteCalendarMember($this_schedule_request,
false) !==
false ) {
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.');
762 return $scheduling_actions;
775function import_collection( $import_content, $user_no, $path, $caldav_context, $appending =
false ) {
778 if ( ! ini_get(
'open_basedir') && (isset($c->dbg[
'ALL']) || isset($c->dbg[
'put'])) ) {
779 $fh = fopen(
'/var/log/davical/PUT-2.debug',
'w');
781 fwrite($fh,$import_content);
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 );
793 $cache = getCacheInstance();
794 $cache_ns = 'collection-'.preg_replace( '{/[^/]*$}
', '/
', $path);
795 $cache->delete( $cache_ns, null );
798 dbg_error_log('PUT
', 'Can only
import files which are VCARD or VCALENDAR
');
810function import_addressbook_collection( $vcard_content, $user_no, $path, $caldav_context, $appending = false ) {
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");
815 require_once('vcard.php
');
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 ));
824 $collection = $qry->Fetch();
825 $collection_id = $collection->collection_id;
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
831 $current_data = array();
832 while( $row = $qry->Fetch() )
833 $current_data[$row->dav_name] = $row->caldav_data;
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
'
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 )
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
853 $resources = $addressbook->GetComponents();
854 if ( count($resources) > 0 )
855 $qry->QDo('SELECT new_sync_token(0,
'.$collection_id.')
');
857 foreach( $resources AS $k => $resource ) {
858 if ( isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import ) $qry->Begin();
860 $vcard = new vCard( $resource->Render() );
862 $uid = $vcard->GetPValue('UID
');
865 $vcard->AddProperty('UID
',$uid);
868 $last_modified = $vcard->GetPValue('REV
');
869 if ( empty($last_modified) ) {
870 $last_modified = gmdate( 'Ymd\THis\Z
' );
871 $vcard->AddProperty('REV
',$last_modified);
874 $created = $vcard->GetPValue('X-CREATED
');
875 if ( empty($last_modified) ) {
876 $created = gmdate( 'Ymd\THis\Z
' );
877 $vcard->AddProperty('X-CREATED
',$created);
880 $rendered_card = $vcard->Render();
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) );
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;
895 if ( isset($current_data[$dav_name]) ) {
896 if ( $rendered_card == $current_data[$dav_name] ) {
897 unset($current_data[$dav_name]);
901 unset($current_data[$dav_name]);
907 if ( isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import ) $qry->Begin();
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));
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;
919 $vcard->Write( $row->dav_id, !$inserting );
921 $qry->QDo(
"SELECT write_sync_change( $collection_id, $sync_change, :dav_name)", array(
':dav_name' => $dav_name ) );
923 if ( isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import ) $qry->Commit();
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);
934 if ( isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import ) $qry->Commit();
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');
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);
962 if ( isset($_GET[
'after']) ) {
963 $after = $_GET[
'after'];
964 if ( strtoupper(substr($after, 0, 1)) ==
'P' || strtoupper(substr($after, 0, 1)) ==
'-P' ) {
966 $duration = $duration->asSeconds();
967 $after = time() - (abs($duration));
971 $after = $after->epoch();
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 );
984 foreach( $timezones AS $k => $tz ) {
985 $tz_ids[$tz->GetPValue(
'TZID')] = $k;
989 $resources = array();
990 foreach( $components AS $k => $comp ) {
991 $uid = $comp->GetPValue(
'UID');
992 if ( $uid ==
null || $uid ==
'' ) {
994 $comp->AddProperty(
'UID',$uid);
995 dbg_error_log(
'LOG WARN',
' PUT: New collection resource does not have a UID - we assign one!' );
997 if ( !isset($resources[$uid]) ) $resources[$uid] = array();
998 $resources[$uid][] = $comp;
1001 $tzid = GetTZID($comp);
1002 if ( !empty($tzid) && !isset($resources[$uid][$tzid]) && isset($tz_ids[$tzid]) ) {
1003 $resources[$uid][$tzid] = $timezones[$tz_ids[$tzid]];
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 ));
1015 $collection = $qry->Fetch();
1016 $collection_id = $collection->collection_id;
1019 $qry->QDo(
'SELECT dav_name, caldav_data FROM caldav_data WHERE collection_id=:collection_id', array(
1020 ':collection_id' => $collection_id
1022 $current_data = array();
1023 while( $row = $qry->Fetch() )
1024 $current_data[$row->dav_name] = $row->caldav_data;
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 );
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 )
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
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)
1050 $calitem_update = <<<EOSQL
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,
1058WHERE collection_id=:collection_id AND dav_name=:dav_name
1062 if ( count($resources) > 0 )
1063 $qry->QDo(
'SELECT new_sync_token(0,'.$collection_id.
')');
1065 foreach( $resources AS $uid => $resource ) {
1068 $vcal =
new vCalendar();
1069 $vcal->SetComponents($resource);
1070 $icalendar = $vcal->Render();
1071 $dav_name = sprintf(
'%s%s.ics', $path, preg_replace(
'{[&?\\/@%+:]}',
'',$uid) );
1073 if ( isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import ) $qry->Begin();
1076 $first = $resource[0];
1078 $dav_data_params = $base_params;
1079 $dav_data_params[
':user_no'] = $user_no;
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;
1088 $dtstart = $first->GetPValue(
'DTSTART');
1089 $calitem_params[
':dtstart_orig'] = $dtstart;
1090 $calitem_params[
':dtstart'] = $dtstart;
1092 if ( (!isset($dtstart) || $dtstart ==
'') && $first->GetPValue(
'DUE') !=
'' ) {
1093 $dtstart = $first->GetPValue(
'DUE');
1094 if ( isset($after) ) $dtstart_date =
new RepeatRuleDateTime($first->GetProperty(
'DUE'));
1096 else if ( isset($after) ) {
1100 $calitem_params[
':rrule'] = $first->GetPValue(
'RRULE');
1103 if ( isset($after) && empty($calitem_params[
':rrule']) && $dtstart_date->epoch() < $after )
continue;
1107 if ( isset($current_data[$dav_name]) ) {
1108 if ( $icalendar == $current_data[$dav_name] ) {
1109 if ( $after ==
null ) {
1110 unset($current_data[$dav_name]);
1115 unset($current_data[$dav_name]);
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));
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;
1131 $dtend = $first->GetPValue(
'DTEND');
1132 $calitem_params[
':dtend_orig'] = $dtend;
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;
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;
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);
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)';
1171 $dtend =
':dtstart';
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;
1179 $dtstamp = $first->GetPValue(
'DTSTAMP');
1180 if ( empty($dtstamp) ) $dtstamp = $last_modified;
1181 $calitem_params[
':dtstamp'] = $dtstamp;
1184 $class = ($collection->public_events_only ==
't' && isset($class) ?
'PUBLIC' : $first->GetPValue(
'CLASS') );
1185 $calitem_params[
':class'] = $class;
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;
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');
1208 $qry->QDo(
'INSERT INTO timezones (tzid, olson_name, active, vtimezone, last_modified) VALUES(:tzid,:olson_name,false,:vtimezone,:last_modified)', $params );
1212 $tz = $olson = $tzid =
null;
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');
1233 $created = $first->GetPValue(
'CREATED');
1234 if ( $created ==
'00001231T000000Z' ) $created =
'20001231T000000Z';
1235 $calitem_params[
':created'] = $created;
1239 if ( !$qry->QDo($sql,$calitem_params) ) rollback_on_error( $caldav_context, $user_no, $path);
1241 write_alarms($dav_id, $first);
1242 write_attendees($dav_id, $vcal);
1244 $qry->QDo(
"SELECT write_sync_change( $collection_id, $sync_change, :dav_name)", array(
':dav_name' => $dav_name ) );
1246 do_scheduling_requests( $vcal,
true );
1247 if ( isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import ) $qry->Commit();
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);
1258 if ( isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import ) $qry->Commit();
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);
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__);
1279 $alarms = $ical->GetComponents(
'VALARM');
1280 if ( count($alarms) < 1 )
return;
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 )' );
1287 foreach( $alarms AS $v ) {
1288 $trigger = array_merge($v->GetProperties(
'TRIGGER'));
1289 if ( $trigger ==
null )
continue;
1290 $trigger = $trigger[0];
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');
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';
1306 $related_trigger = preg_replace(
'{(\d+[WDHMS])}',
'-$1 ', $related_trigger );
1309 $related_trigger = preg_replace(
'{(\d+[WDHMS])}',
'$1 ', $related_trigger );
1312 else if ( $trigger_type ==
'DATE-TIME' ) {
1313 $related = $trigger;
1316 if (
false === strtotime($trigger->Value()) )
continue;
1317 $related = $trigger;
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__);
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__);
1344 $attendees = $ical->GetAttendees();
1345 if ( count($attendees) < 1 )
return;
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 )' );
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() );
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();
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;
1391 $path = $resource->bound_from();
1392 $user_no = $collection->user_no();
1393 $vcal =
new vCalendar( $caldav_data );
1394 $resources = $vcal->GetComponents(
'VTIMEZONE',
false);
1395 if ( !isset($resources[0]) ) {
1396 $resource_type =
'Unknown';
1398 rollback_on_error( $caldav_context, $user_no, $path, translate(
'No calendar content'), 412 );
1402 $first = $resources[0];
1403 if ( !($first instanceof vComponent) ) {
1404 print $vcal->Render();
1405 fatal(
'This is not a vComponent!');
1407 $resource_type = $first->GetType();
1410 $collection_id = $collection->collection_id();
1412 $qry =
new AwlQuery();
1415 $calitem_params = array(
1419 if ( $put_action_type ==
'INSERT' ) {
1420 $qry->QDo(
'SELECT nextval(\'dav_id_seq\') AS dav_id, null AS caldav_data');
1423 $qry->QDo(
'SELECT dav_id, caldav_data FROM caldav_data WHERE dav_name = :dav_name ', array(
':dav_name' => $path));
1425 if ( $qry->rows() != 1 || !($row = $qry->Fetch()) ) {
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);
1431 $dav_id = $row->dav_id;
1432 $old_dav_data = $row->caldav_data;
1435 if ($old_dav_data) {
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");
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.");
1467 $dav_params = array(
1469 ':dav_data' => $caldav_data,
1470 ':caldav_type' => $resource_type,
1471 ':session_user' => $author,
1472 ':weak_etag' => $weak_etag
1475 $dav_params[
':dav_id'] = $dav_id;
1476 $calitem_params[
':dav_id'] = $dav_id;
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;
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;
1489 $dtend = $first->GetPValue(
'DTEND');
1490 $calitem_params[
':dtend_orig'] = $dtend;
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;
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;
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)';
1528 $dtend =
':dtstart';
1532 $dtstamp = $first->GetPValue(
'DTSTAMP');
1533 if ( !isset($dtstamp) || $dtstamp ==
'' ) {
1535 $dtstamp = gmdate(
'Ymd\THis\Z' );
1537 $calitem_params[
':dtstamp'] = $dtstamp;
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;
1544 $created = $first->GetPValue(
'CREATED');
1545 if ( $created ==
'00001231T000000Z' ) $created =
'20001231T000000Z';
1547 $class = $first->GetPValue(
'CLASS');
1550 if ( public_events_only($user_no, $path) && isset($class) ) {
1554 $calitem_params[
':class'] = $class;
1557 $last_olson =
'Turkmenikikamukau';
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 );
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);
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.
"'" );
1584 $last_olson = $olson;
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 );
1593 if ( !isset($olson) || $olson ==
'' ) $olson = $tzid;
1597 $qry->QDo(
'SELECT new_sync_token(0,'.$collection_id.
')');
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');
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;
1616 $vcal->Render(
null,
true);
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);
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);
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';
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);
1646 if ( $put_action_type ==
'INSERT' ) {
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,
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,
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'];
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
1680 write_alarms($dav_id, $first);
1681 write_attendees($dav_id, $vcal);
1683 if ( $log_action && function_exists(
'log_caldav_action') ) {
1684 log_caldav_action( $put_action_type, $first->GetPValue(
'UID'), $user_no, $collection_id, $path );
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 );
1691 $qry =
new AwlQuery( $sql, $calitem_params );
1692 if ( !$qry->Exec(
'PUT',__LINE__,__FILE__) ) {
1693 rollback_on_error( $caldav_context, $user_no, $path);
1696 $qry->QDo(
"SELECT write_sync_change( :collection_id, $sync_change, :dav_name)",
1697 array(
':collection_id' => $collection_id,
':dav_name' => $path ) );
1700 if ( function_exists(
'post_commit_action') ) {
1701 post_commit_action( $put_action_type, $first->GetPValue(
'UID'), $user_no, $collection_id, $path );
1705 $cache = getCacheInstance();
1706 $cache_ns =
'collection-'.preg_replace(
'{/[^/]*$}',
'/', $path);
1707 $cache->delete( $cache_ns,
null );
1709 dbg_error_log(
'PUT',
'User: %d, ETag: %s, Path: %s', $author, $etag, $path);
1725function simple_write_resource( $path, $caldav_data, $put_action_type, $write_action_log =
false ) {
1732 $etag = md5($caldav_data);
1733 $collection_path = preg_replace(
'#/[^/]*$#',
'/', $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 );