From 50fccc73d8f45fc0235286c120ecb0853d18f0b2 Mon Sep 17 00:00:00 2001 From: Andrew McMillan Date: Mon, 30 Aug 2010 18:55:23 +1200 Subject: [PATCH] Working freebusy refactored to use a single core routine. --- htdocs/freebusy.php | 48 ++++++++++--- inc/caldav-POST.php | 124 +++++---------------------------- inc/caldav-REPORT-freebusy.php | 7 +- inc/freebusy-functions.php | 41 ++++++----- 4 files changed, 85 insertions(+), 135 deletions(-) diff --git a/htdocs/freebusy.php b/htdocs/freebusy.php index a97e8635..887a40da 100644 --- a/htdocs/freebusy.php +++ b/htdocs/freebusy.php @@ -11,6 +11,7 @@ else { $session = new HTTPAuthSession(); } + /** * Submission parameters recommended by calconnect, plus some generous alternatives */ @@ -27,26 +28,52 @@ if ( !isset($fb_start) || $fb_start == '' ) $fb_start = date('Y-m-d\TH:i:s', t if ( (!isset($fb_period) && !isset($fb_end)) || ($fb_period == '' && $fb_end == '') ) $fb_period = 'P44D'; // 44 days - 2 days more than recommended default + +/** +* If fb_user (user, userid, user_no or email parameter) then we adjust +* the path of the request to suit. +*/ +if ( isset($fb_user) ) $_SERVER['PATH_INFO'] = '/'.$fb_user.'/'; + +/** +* We also allow URLs like .../freebusy.php/user@example.com to work, so long as +* the e-mail matches a single user whose calendar we have rights to. +* @NOTE: It is OK for there to *be* duplicate e-mail addresses, just so long as we +* only have read permission (or more) for only one of them. +*/ require_once("CalDAVRequest.php"); +$request = new CalDAVRequest(array("allow_by_email" => 1)); +$path_match = '^'.$request->path; +if ( preg_match( '{^/(\S+@[a-z0-9][a-z0-9-]*[.][a-z0-9.-]+)/?$}i', $request->path, $matches ) ) { + $u = getUserByEMail($matches[1]); + $path_match = '^/'.$u->username.'/'; +} if ( isset($fb_format) && $fb_format != 'text/calendar' ) { $request->DoResponse( 406, 'This server only supports the text/calendar format for freebusy URLs' ); } - -/** -* We also allow URLs like .../freebusy.php/user@example.com to work, so long as -* the e-mail matches a single user whose calendar we have rights to. -* NOTE: It is OK for there to *be* duplicate e-mail addresses, just so long as we -* only have read permission (or more) for only one of them. -*/ -$request = new CalDAVRequest(array("allow_by_email" => 1)); - if ( ! $request->HavePrivilegeTo('read-free-busy') ) $request->DoResponse( 404 ); +require_once("freebusy-functions.php"); + switch ( $_SERVER['REQUEST_METHOD'] ) { case 'GET': - include_once("freebusy-GET.php"); + $range_start = new RepeatRuleDateTime($fb_start); + if ( !isset($fb_end) ) { + $range_end = clone($range_start); + $range_end->modify($fb_period); + } + else { + $range_end = new RepeatRuleDateTime($fb_end); + } + $freebusy = get_freebusy( $path_match, $range_start, $range_end ); + + $result = new iCalComponent(); + $result->VCalendar(); + $result->AddComponent($freebusy); + + $request->DoResponse( 200, $result->Render(), 'text/calendar' ); break; default: @@ -56,4 +83,3 @@ switch ( $_SERVER['REQUEST_METHOD'] ) { dbg_error_log( "freebusy", "RAW: %s", str_replace("\n", "",str_replace("\r", "", $raw_post)) ); } - diff --git a/inc/caldav-POST.php b/inc/caldav-POST.php index a7e04f99..96520cdc 100644 --- a/inc/caldav-POST.php +++ b/inc/caldav-POST.php @@ -12,8 +12,8 @@ dbg_error_log("POST", "method handler"); require_once("XMLDocument.php"); require_once("iCalendar.php"); -include_once("RRule.php"); include_once('caldav-PUT-functions.php'); +include_once('freebusy-functions.php'); if ( ! $request->AllowedTo("CALDAV:schedule-send-freebusy") && ! $request->AllowedTo("CALDAV:schedule-send-invite") @@ -39,6 +39,13 @@ function handle_freebusy_request( $ic ) { $fbq_start = $ic->GetPValue('DTSTART'); $fbq_end = $ic->GetPValue('DTEND'); + if ( ! ( isset($fbq_start) || isset($fbq_end) ) ) { + $request->DoResponse( 400, 'All valid freebusy requests MUST contain a DTSTART and a DTEND' ); + } + + $range_start = new RepeatRuleDateTime($fbq_start); + $range_end = new RepeatRuleDateTime($fbq_end); + $attendees = $ic->GetProperties('ATTENDEE'); if ( preg_match( '# iCal/\d#', $_SERVER['HTTP_USER_AGENT']) ) { dbg_error_log( "POST", "Non-compliant iCal request. Using X-WR-ATTENDEE property" ); @@ -52,18 +59,19 @@ function handle_freebusy_request( $ic ) { foreach( $attendees AS $k => $attendee ) { $attendee_email = preg_replace( '/^mailto:/', '', $attendee->Value() ); dbg_error_log( "POST", "Calculating free/busy for %s", $attendee_email ); - if ( ! ( isset($fbq_start) || isset($fbq_end) ) ) { - $request->DoResponse( 400, 'All valid freebusy requests MUST contain a DTSTART and a DTEND' ); - } /** @TODO: Refactor this so we only do one query here and loop through the results */ $params = array( ':session_principal' => $session->principal_id, ':scan_depth' => $c->permission_scan_depth, ':email' => $attendee_email ); - $qry = new AwlQuery('SELECT pprivs(:session_principal::int8,principal_id,:scan_depth::int) AS p FROM usr JOIN principal USING(user_no) WHERE lower(usr.email) = lower(:email)', $params ); + $qry = new AwlQuery('SELECT pprivs(:session_principal::int8,principal_id,:scan_depth::int) AS p, username FROM usr JOIN principal USING(user_no) WHERE lower(usr.email) = lower(:email)', $params ); if ( !$qry->Exec('POST',__LINE__,__FILE__) ) $request->DoResponse( 501, 'Database error'); if ( $qry->rows() > 1 ) { // Unlikely, but if we get more than one result we'll do an exact match instead. - if ( !$qry->QDo('SELECT pprivs(:session_principal::int8,principal_id,:scan_depth::int) AS p FROM usr JOIN principal USING(user_no) WHERE usr.email = :email', $params ) ) + if ( !$qry->QDo('SELECT pprivs(:session_principal::int8,principal_id,:scan_depth::int) AS p, username FROM usr JOIN principal USING(user_no) WHERE usr.email = :email', $params ) ) $request->DoResponse( 501, 'Database error'); + if ( $qry->rows() == 0 ) { + /** Sigh... Go back to the original case-insensitive match */ + $qry->QDo('SELECT pprivs(:session_principal::int8,principal_id,:scan_depth::int) AS p, username FROM usr JOIN principal USING(user_no) WHERE lower(usr.email) = lower(:email)', $params ); + } } $response = $reply->NewXMLElement("response", false, false, 'urn:ietf:params:xml:ns:caldav'); @@ -75,115 +83,19 @@ function handle_freebusy_request( $ic ) { $responses[] = $response; continue; } - if ( ! $userperms = $qry->Fetch() ) $request->DoResponse( 501, 'Database error'); - if ( (privilege_to_bits('schedule-query-freebusy') & bindec($userperms->p)) == 0 ) { + if ( ! $attendee_usr = $qry->Fetch() ) $request->DoResponse( 501, 'Database error'); + if ( (privilege_to_bits('schedule-query-freebusy') & bindec($attendee_usr->p)) == 0 ) { $reply->CalDAVElement($response, "request-status", "3.8;No authority" ); $reply->CalDAVElement($response, "calendar-data" ); $responses[] = $response; continue; } + $attendee_path_match = '^/'.$attendee_usr->username.'/'; + $fb = get_freebusy( $attendee_path_match, $range_start, $range_end, bindec($attendee_usr->p) ); - // If we make it here, then it seems we are allowed to see their data... - $where = " WHERE lower(usr.email) = :email AND collection.is_calendar "; - $params = array( ':email' => strtolower($attendee_email) ); - if ( isset( $fbq_start ) || isset( $fbq_end ) ) { - $params[':start'] = $fbq_start; - $params[':finish'] = $fbq_end; - $where .= "AND rrule_event_overlaps( dtstart, dtend, rrule, :start, :finish ) "; - } - $where .= "AND caldav_data.caldav_type IN ( 'VEVENT', 'VFREEBUSY' ) "; - $where .= "AND (calendar_item.transp != 'TRANSPARENT' OR calendar_item.transp IS NULL) "; - $where .= "AND (calendar_item.status != 'CANCELLED' OR calendar_item.status IS NULL) "; - - /** - * Only know about PRIVATE events if you have *full* permission to the calendar - */ - if ( bindec($userperms->p) != privilege_to_bits('all') ) { - $where .= "AND (calendar_item.class != 'PRIVATE' OR calendar_item.class IS NULL) "; - } - - $busy = array(); - $busy_tentative = array(); - /** @TODO prove this is correct */ - $sql = "SELECT caldav_data.caldav_data, calendar_item.rrule, calendar_item.transp, calendar_item.status, "; - $sql .= "to_char(calendar_item.dtstart at time zone 'GMT',".iCalendar::SqlUTCFormat().") AS start, "; - $sql .= "to_char(calendar_item.dtend at time zone 'GMT',".iCalendar::SqlUTCFormat().") AS finish "; - $sql .= "FROM usr INNER JOIN collection USING (user_no) INNER JOIN caldav_data USING (collection_id) INNER JOIN calendar_item USING(dav_id)".$where; - if ( isset($c->strict_result_ordering) && $c->strict_result_ordering ) $sql .= " ORDER BY dav_id"; - $qry = new AwlQuery( $sql, $params ); - if ( $qry->Exec("POST",__LINE__,__FILE__) && $qry->rows() > 0 ) { - while( $calendar_object = $qry->Fetch() ) { - if ( $calendar_object->transp != "TRANSPARENT" ) { - switch ( $calendar_object->status ) { - case "TENTATIVE": - dbg_error_log( "POST", " FreeBusy: tentative appointment: %s, %s", $calendar_object->start, $calendar_object->finish ); - $busy_tentative[] = $calendar_object; - break; - - case "CANCELLED": - // Cancelled events are ignored - break; - - default: - dbg_error_log( "POST", " FreeBusy: Not transparent, tentative or cancelled: %s, %s", $calendar_object->start, $calendar_object->finish ); - $busy[] = $calendar_object; - break; - } - } - } - } - - - $i = 0; - - $fb = new iCalComponent(); - $fb->AddProperty( 'DTSTAMP', gmdate('Ymd\THis\Z') ); - $fb->AddProperty( 'DTSTART', $fbq_start ); - $fb->AddProperty( 'DTEND', $fbq_end ); $fb->AddProperty( 'UID', $ic->GetPValue('UID') ); $fb->SetProperties( $ic->GetProperties('ORGANIZER'), 'ORGANIZER'); - $fb->SetType('VFREEBUSY'); - $fb->AddProperty( $attendee ); - $fbparams = array( "FBTYPE" => "BUSY-TENTATIVE" ); - foreach( $busy_tentative AS $k => $v ) { - $start = new iCalDate($v->start); - $duration = $start->DateDifference($v->finish); - if ( $v->rrule != "" ) { - $rrule = new RRule( $start, $v->rrule ); - while ( $date = $rrule->GetNext() ) { - if ( ! $date->GreaterThan($fbq_start) ) continue; - if ( $date->GreaterThan($fbq_end) ) break; - $todate = clone($date); - $todate->AddDuration($duration); - $fb->AddProperty("FREEBUSY", sprintf("%s/%s", $date->RenderGMT(), $todate->RenderGMT() ), $fbparams); - } - } - else { - $finish = new iCalDate($v->finish); - $fb->AddProperty("FREEBUSY", sprintf("%s/%s", $start->RenderGMT(), $finish->RenderGMT() ), $fbparams ); - } - } - - $fbparams = array( "FBTYPE" => "BUSY" ); - foreach( $busy AS $k => $v ) { - $start = new iCalDate($v->start); - $duration = $start->DateDifference($v->finish); - if ( $v->rrule != "" ) { - $rrule = new RRule( $start, $v->rrule ); - while ( $date = $rrule->GetNext() ) { - if ( ! $date->GreaterThan($fbq_start) ) continue; - if ( $date->GreaterThan($fbq_end) ) break; - $todate = clone($date); - $todate->AddDuration($duration); - $fb->AddProperty("FREEBUSY", sprintf("%s/%s", $date->RenderGMT(), $todate->RenderGMT() ), $fbparams ); - } - } - else { - $finish = new iCalDate($v->finish); - $fb->AddProperty("FREEBUSY", sprintf("%s/%s", $start->RenderGMT(), $finish->RenderGMT() ), $fbparams ); - } - } $vcal = new iCalComponent(); $vcal->VCalendar( array('METHOD' => 'REPLY') ); diff --git a/inc/caldav-REPORT-freebusy.php b/inc/caldav-REPORT-freebusy.php index 256b02ff..b4c8acef 100644 --- a/inc/caldav-REPORT-freebusy.php +++ b/inc/caldav-REPORT-freebusy.php @@ -15,9 +15,12 @@ $range_end = new RepeatRuleDateTime($fbq_end); /** We use the same code for the REPORT, the POST and the freebusy GET... */ -$freebusy = get_freebusy( $request->path.$request->DepthRegexTail(), $range_start, $range_end ); +$freebusy = get_freebusy( '^'.$request->path.$request->DepthRegexTail(), $range_start, $range_end ); +$result = new iCalComponent(); +$result->VCalendar(); +$result->AddComponent($freebusy); -$request->DoResponse( 200, $freebusy, 'text/calendar' ); +$request->DoResponse( 200, $result->Render(), 'text/calendar' ); // Won't return from that diff --git a/inc/freebusy-functions.php b/inc/freebusy-functions.php index fdb4df63..caf88a3a 100644 --- a/inc/freebusy-functions.php +++ b/inc/freebusy-functions.php @@ -6,33 +6,42 @@ * a freebusy GET request. */ -include_once("RRule-v2.php"); +include_once('iCalendar.php'); +include_once('RRule-v2.php'); -function get_freebusy( $path_match, $range_start, $range_end ) { +function get_freebusy( $path_match, $range_start, $range_end, $bin_privs = null ) { global $request; +// printf( "Path: %s\n", $path_match); +// print_r($range_start); +// print_r($range_end); + + if ( !isset($bin_privs) ) $bin_privs = $request->Privileges(); if ( !isset($range_start) || !isset($range_end) ) { $request->DoResponse( 400, 'All valid freebusy requests MUST contain a time-range filter' ); } - $params = array( ':path_match' => '^'.$path_match, ':start' => $range_start->UTC(), ':end' => $range_end->UTC() ); + $params = array( ':path_match' => $path_match, ':start' => $range_start->UTC(), ':end' => $range_end->UTC() ); $where = ' WHERE caldav_data.dav_name ~ :path_match '; $where .= 'AND rrule_event_overlaps( dtstart, dtend, rrule, :start, :end) '; $where .= "AND caldav_data.caldav_type IN ( 'VEVENT', 'VTODO' ) "; $where .= "AND (calendar_item.transp != 'TRANSPARENT' OR calendar_item.transp IS NULL) "; $where .= "AND (calendar_item.status != 'CANCELLED' OR calendar_item.status IS NULL) "; + $where .= "AND collection.is_calendar AND collection.schedule_transp = 'opaque' "; - if ( $request->Privileges() != privilege_to_bits('all') ) { + if ( $bin_privs != privilege_to_bits('all') ) { $where .= "AND (calendar_item.class != 'PRIVATE' OR calendar_item.class IS NULL) "; } $fbtimes = array(); - $sql = "SELECT caldav_data.caldav_data, calendar_item.rrule, calendar_item.transp, calendar_item.status, "; - $sql .= "to_char(calendar_item.dtstart at time zone 'GMT',".iCalendar::SqlUTCFormat().") AS start, "; - $sql .= "to_char(calendar_item.dtend at time zone 'GMT',".iCalendar::SqlUTCFormat().") AS finish "; - $sql .= "FROM caldav_data INNER JOIN calendar_item USING(dav_id,user_no,dav_name)".$where; - if ( isset($c->strict_result_ordering) && $c->strict_result_ordering ) $sql .= " ORDER BY dav_id"; + $sql = 'SELECT caldav_data.caldav_data, calendar_item.rrule, calendar_item.transp, calendar_item.status, '; + $sql .= "to_char(calendar_item.dtstart at time zone 'GMT',".iCalendar::SqlUTCFormat().') AS start, '; + $sql .= "to_char(calendar_item.dtend at time zone 'GMT',".iCalendar::SqlUTCFormat().') AS finish '; + $sql .= 'FROM caldav_data INNER JOIN calendar_item USING(dav_id,user_no,dav_name,collection_id) '; + $sql .= 'INNER JOIN collection USING(collection_id)'; + $sql .= $where; + if ( isset($c->strict_result_ordering) && $c->strict_result_ordering ) $sql .= ' ORDER BY dav_id'; $qry = new AwlQuery( $sql, $params ); if ( $qry->Exec("REPORT",__LINE__,__FILE__) && $qry->rows() > 0 ) { while( $calendar_object = $qry->Fetch() ) { @@ -43,13 +52,18 @@ function get_freebusy( $path_match, $range_start, $range_end ) { dbg_error_log( "REPORT", " FreeBusy: Not transparent, tentative or cancelled: %s, %s", $calendar_object->start, $calendar_object->finish ); $ics = new vComponent($calendar_object->caldav_data); $expanded = expand_event_instances($ics, $range_start, $range_end); - $expansion = $expanded->GetComponents( array('VEVENT','VTODO','VJOURNAL') ); + $expansion = $expanded->GetComponents( array('VEVENT'=>true,'VTODO'=>true,'VJOURNAL'=>true) ); foreach( $expansion AS $k => $v ) { +// echo "=====================================================\n"; +// printf( "Type: %s\n", $v->GetType()); +// echo "-----------------------------------------------------\n"; $dtstart = $v->GetProperty('DTSTART'); +// print_r($dtstart); $start_date = new RepeatRuleDateTime($dtstart->Value()); $duration = $v->GetProperty('DURATION'); $end_date = clone($start_date); $end_date->modify( $duration->Value() ); + if ( $end_date < $range_start || $start_date > $range_end ) continue; $thisfb = $start_date->UTC() .'/'. $end_date->UTC() . $extra; array_push( $fbtimes, $thisfb ); } @@ -68,11 +82,6 @@ function get_freebusy( $path_match, $range_start, $range_end ) { $freebusy->AddProperty( 'FREEBUSY', $text[0], (isset($text[1]) ? array('FBTYPE' => $text[1]) : null) ); } - - $result = new iCalComponent(); - $result->VCalendar(); - $result->AddComponent($freebusy); - - return $result->Render(); + return $freebusy; }