From fa67ef987e36d3133ac7ec367698629153fa3a50 Mon Sep 17 00:00:00 2001 From: Andrew McMillan Date: Mon, 30 Aug 2010 09:08:17 +1200 Subject: [PATCH] Refactoring free/busy handling to a single core routine with RRule-2 --- inc/RRule-v2.php | 105 +++++++++--------- inc/caldav-REPORT-freebusy.php | 93 +--------------- inc/freebusy-functions.php | 78 +++++++++++++ .../0886-REPORT-freebusy.test | 33 ++++++ 4 files changed, 172 insertions(+), 137 deletions(-) create mode 100644 inc/freebusy-functions.php create mode 100644 testing/tests/regression-suite/0886-REPORT-freebusy.test diff --git a/inc/RRule-v2.php b/inc/RRule-v2.php index 22314eb2..4f82cba4 100644 --- a/inc/RRule-v2.php +++ b/inc/RRule-v2.php @@ -37,42 +37,34 @@ $GLOBALS['debug_rrule'] = false; * Wrap the DateTimeZone class to allow parsing some iCalendar TZID strangenesses */ class RepeatRuleTimeZone extends DateTimeZone { - private $tzid; + private $tz_defined; public function __construct($dtz = null) { - $this->tzid = false; + $this->tz_defined = false; if ( !isset($dtz) ) return; + $dtz = olson_from_tzstring($dtz); + try { parent::__construct($dtz); - $this->tzid = $dtz; + $this->tz_defined = $dtz; } catch (Exception $e) { $original = $dtz; - $dtz = olson_from_tzstring($dtz); - if ( isset($dtz) ) { - try { - parent::__construct($dtz); - $this->tzid = $dtz; - } - catch (Exception $e) { - dbg_error_log( 'ERROR', 'Could not parse timezone "%s" - will use floating time', $original ); - $dtz = new DateTimeZone('UTC'); - $this->tzid = false; - } - } - else { + + if ( !isset($dtz) ) { dbg_error_log( 'ERROR', 'Could not parse timezone "%s" - will use floating time', $original ); $dtz = new DateTimeZone('UTC'); - $this->tzid = false; + $this->tz_defined = false; } } } function tzid() { - $tzid = parent::getName(); + if ( $this->tz_defined === false ) return false; + $tzid = $this->getName(); if ( $tzid != 'UTC' ) return $tzid; - return $this->tzid; + return $this->tz_defined; } } @@ -91,6 +83,8 @@ class RepeatRuleDateTime extends DateTime { public function __construct($date = null, $dtz = null) { $this->is_date = false; + if ( !isset($date) ) return; + if ( preg_match('{;?VALUE=DATE[:;]}', $date, $matches) ) $this->is_date = true; elseif ( preg_match('{:([12]\d{3}) (0[1-9]|1[012]) (0[1-9]|[12]\d|3[01]Z?) $}x', $date, $matches) ) $this->is_date = true; if (preg_match('/;?TZID=([^:]+).*:(\d{8}(T\d{6})?)(Z)?/', $date, $matches) ) { @@ -150,7 +144,8 @@ class RepeatRuleDateTime extends DateTime { if ( isset($matches[6]) && $matches[6] != '' ) $interval .= $minus . $matches[7] . ' minutes '; if ( isset($matches[8]) && $matches[8] != '' ) $interval .= $minus . $matches[9] . ' seconds '; } - return (string)parent::modify($interval); + parent::modify($interval); + return $this->__toString(); } @@ -184,6 +179,11 @@ class RepeatRuleDateTime extends DateTime { } + public function RFC5545Duration( $end_stamp ) { + return sprintf( 'PT%dM', intval(($end_stamp->epoch() - $this->epoch()) / 60) ); + } + + public function setTimeZone( $tz ) { if ( is_string($tz) ) { $tz = new RepeatRuleTimeZone($tz); @@ -655,13 +655,13 @@ class RepeatRule { } -require_once("iCalendar.php"); +require_once("vComponent.php"); /** * Expand the event instances for an RDATE or EXDATE property * * @param string $property RDATE or EXDATE, depending... -* @param array $component An iCalComponent which is applies for these instances +* @param array $component A vComponent which is a VEVENT, VTODO or VJOURNAL * @param array $range_end A date after which we care less about expansion * * @return array An array keyed on the UTC dates, referring to the component @@ -688,7 +688,7 @@ function rdate_expand( $dtstart, $property, $component, $range_end = null ) { * * @param object $dtstart A RepeatRuleDateTime which is the master dtstart * @param string $property RDATE or EXDATE, depending... -* @param array $component An iCalComponent which is applies for these instances +* @param array $component A vComponent which is a VEVENT, VTODO or VJOURNAL * @param array $range_end A date after which we care less about expansion * * @return array An array keyed on the UTC dates, referring to the component @@ -696,13 +696,14 @@ function rdate_expand( $dtstart, $property, $component, $range_end = null ) { function rrule_expand( $dtstart, $property, $component, $range_end ) { $expansion = array(); - $recur = $component->GetPValue($property); + $recur = $component->GetProperty($property); if ( !isset($recur) ) return $expansion; + $recur = $recur->Value(); - $this_start = $component->GetPValue('DTSTART'); + $this_start = $component->GetProperty('DTSTART'); if ( isset($this_start) ) { - $timezone = $component->GetPParamValue('DTSTART', 'TZID'); - $this_start = new RepeatRuleDateTime($this_start,$timezone); + $timezone = $this_start->GetParameterValue('TZID'); + $this_start = new RepeatRuleDateTime($this_start->Value(),$timezone); } else { $this_start = clone($dtstart); @@ -722,14 +723,14 @@ function rrule_expand( $dtstart, $property, $component, $range_end ) { /** * Expand the event instances for an iCalendar VEVENT (or VTODO) * -* @param object $ics An iCalComponent which is the master VCALENDAR -* @param object $range_start A RepeatRuleDateTime which is the beginning of the range for events -* @param object $range_end A RepeatRuleDateTime which is the end of the range for events +* @param object $ics A vComponent which is a VCALENDAR containing components needing expansion +* @param object $range_start A RepeatRuleDateTime which is the beginning of the range for events, default -6 weeks +* @param object $range_end A RepeatRuleDateTime which is the end of the range for events, default +6 weeks * -* @return iCalComponent The original iCalComponent with expanded events in the range. +* @return vComponent The original vComponent, with the instances of the internal components expanded. */ -function expand_event_instances( $ics, $range_start = null, $range_end = null ) { - $components = $ics->GetComponents(); +function expand_event_instances( $vResource, $range_start = null, $range_end = null ) { + $components = $vResource->GetComponents(); if ( !isset($range_start) ) { $range_start = new RepeatRuleDateTime(); $range_start->modify('-6 weeks'); } if ( !isset($range_end) ) { $range_end = clone($range_start); $range_end->modify('+6 months'); } @@ -745,15 +746,16 @@ function expand_event_instances( $ics, $range_start = null, $range_end = null ) continue; } if ( !isset($dtstart) ) { - $tzid = $comp->GetPParamValue('DTSTART', 'TZID'); - $dtstart = new RepeatRuleDateTime( $comp->GetPValue('DTSTART'), $tzid ); + $dtstart = $comp->GetProperty('DTSTART'); + $tzid = $dtstart->GetParameterValue('TZID'); + $dtstart = new RepeatRuleDateTime( $dtstart->Value(), $tzid ); $instances[$dtstart->UTC()] = $comp; } - $p = $comp->GetPValue('RECURRENCE-ID'); - if ( isset($p) && $p != '' ) { - $range = $comp->GetPParamValue('RECURRENCE-ID', 'RANGE'); - $recur_tzid = $comp->GetPParamValue('RECURRENCE-ID', 'TZID'); - $recur_utc = new RepeatRuleDateTime($p,$recur_tzid); + $p = $comp->GetProperty('RECURRENCE-ID'); + if ( isset($p) && $p->Value() != '' ) { + $range = $p->GetParameterValue('RANGE'); + $recur_tzid = $p->GetParameterValue('TZID'); + $recur_utc = new RepeatRuleDateTime($p->Value(),$recur_tzid); $recur_utc = $recur_utc->UTC(); if ( isset($range) && $range == 'THISANDFUTURE' ) { foreach( $instances AS $k => $v ) { @@ -781,12 +783,16 @@ function expand_event_instances( $ics, $range_start = null, $range_end = null ) if ( $utc > $end_utc ) break; $end_type = ($comp->GetType() == 'VTODO' ? 'DUE' : 'DTEND'); - $duration = $comp->GetPValue('DURATION'); + $duration = $comp->GetProperty('DURATION'); if ( !isset($duration) ) { - if ( !isset($end) ) $end = $comp->GetPValue('DUE'); - $dtend = new RepeatRuleDateTime( $comp->GetPValue($end_type), $comp->GetPParamValue($end_type, 'TZID')); - $dtsrt = new RepeatRuleDateTime( $comp->GetPValue('DTSTART'), $comp->GetPParamValue('DTSTART', 'TZID')); - $duration = sprintf( 'PT%dM', intval(($dtend->epoch() - $dtsrt->epoch()) / 60) ); + $instance_start = $comp->GetProperty('DTSTART'); + $dtsrt = new RepeatRuleDateTime( $instance_start->Value(), $instance_start->GetParameterValue('TZID')); + $instance_end = $comp->GetProperty($end_type); + $dtend = new RepeatRuleDateTime( $instance_end->Value(), $instance_end->GetParameterValue('TZID')); + $duration = $dtstart->RFC5545Duration( $dtend ); + } + else { + $duration = $duration->Value(); } if ( $utc < $start_utc ) { @@ -802,8 +808,7 @@ function expand_event_instances( $ics, $range_start = null, $range_end = null ) } } $component = clone($comp); - $component->ClearProperties('DTSTART'); - $component->ClearProperties($end_type); + $component->ClearProperties( array('DTSTART'=> true, 'DUE' => true, 'DTEND' => true) ); $component->AddProperty('DTSTART', $utc ); $component->AddProperty('DURATION', $duration ); $new_components[] = $component; @@ -811,11 +816,11 @@ function expand_event_instances( $ics, $range_start = null, $range_end = null ) } if ( $in_range ) { - $ics->SetComponents($new_components); + $vResource->SetComponents($new_components); } else { - $ics->SetComponents(array()); + $vResource->SetComponents(array()); } - return $ics; + return $vResource; } diff --git a/inc/caldav-REPORT-freebusy.php b/inc/caldav-REPORT-freebusy.php index 5bc129eb..256b02ff 100644 --- a/inc/caldav-REPORT-freebusy.php +++ b/inc/caldav-REPORT-freebusy.php @@ -2,103 +2,22 @@ /** * Handle the FREE-BUSY-QUERY variant of REPORT */ -include_once("iCalendar.php"); -include_once("RRule.php"); +include_once("freebusy-functions.php"); $fbq_content = $xmltree->GetContent('urn:ietf:params:xml:ns:caldav:free-busy-query'); $fbq_start = $fbq_content[0]->GetAttribute('start'); $fbq_end = $fbq_content[0]->GetAttribute('end'); - if ( ! ( isset($fbq_start) || isset($fbq_end) ) ) { $request->DoResponse( 400, 'All valid freebusy requests MUST contain a time-range filter' ); } -$params = array( ':path_match' => '^'.$request->path.$request->DepthRegexTail(), ':start' => $fbq_start, ':end' => $fbq_end ); -$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', '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) "; +$range_start = new RepeatRuleDateTime($fbq_start); +$range_end = new RepeatRuleDateTime($fbq_end); -if ( $request->Privileges() != privilege_to_bits('all') ) { - $where .= "AND (calendar_item.class != 'PRIVATE' OR calendar_item.class IS NULL) "; -} -$busy = array(); -$busy_tentative = 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::SqlDateFormat().") AS start, "; -$sql .= "to_char(calendar_item.dtend at time zone 'GMT',".iCalendar::SqlDateFormat().") 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"; -$qry = new AwlQuery( $sql, $params ); -if ( $qry->Exec("REPORT",__LINE__,__FILE__) && $qry->rows() > 0 ) { - while( $calendar_object = $qry->Fetch() ) { - if ( $calendar_object->transp != "TRANSPARENT" ) { - switch ( $calendar_object->status ) { - case "TENTATIVE": - dbg_error_log( "REPORT", " FreeBusy: tentative appointment: %s, %s", $calendar_object->start, $calendar_object->finish ); - $busy_tentative[] = $calendar_object; - break; +/** 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 ); - case "CANCELLED": - // Cancelled events are ignored - break; - default: - dbg_error_log( "REPORT", " FreeBusy: Not transparent, tentative or cancelled: %s, %s", $calendar_object->start, $calendar_object->finish ); - $busy[] = $calendar_object; - break; - } - } - } -} - -$freebusy = new iCalComponent(); -$freebusy->SetType('VFREEBUSY'); -$freebusy->AddProperty('DTSTAMP', date('Ymd\THis\Z')); -$freebusy->AddProperty('DTSTART', $fbq_start); -$freebusy->AddProperty('DTEND', $fbq_end); - -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); - $freebusy->AddProperty( 'FREEBUSY', $date->RenderGMT().'/'.$todate->RenderGMT(), array('FBTYPE' => 'BUSY-TENTATIVE') ); - } - } - else { - $freebusy->AddProperty( 'FREEBUSY', $start->RenderGMT().'/'.$v->finish, array('FBTYPE' => 'BUSY-TENTATIVE') ); - } -} - -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); - $freebusy->AddProperty( 'FREEBUSY', $date->RenderGMT().'/'.$todate->RenderGMT() ); - } - } - else { - $freebusy->AddProperty( 'FREEBUSY', $start->RenderGMT().'/'.$v->finish ); - } -} - -$result = new iCalComponent(); -$result->VCalendar(); -$result->AddComponent($freebusy); - -$request->DoResponse( 200, $result->Render(), 'text/calendar' ); +$request->DoResponse( 200, $freebusy, 'text/calendar' ); // Won't return from that diff --git a/inc/freebusy-functions.php b/inc/freebusy-functions.php new file mode 100644 index 00000000..fdb4df63 --- /dev/null +++ b/inc/freebusy-functions.php @@ -0,0 +1,78 @@ +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() ); + $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) "; + + if ( $request->Privileges() != 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"; + $qry = new AwlQuery( $sql, $params ); + if ( $qry->Exec("REPORT",__LINE__,__FILE__) && $qry->rows() > 0 ) { + while( $calendar_object = $qry->Fetch() ) { + $extra = ''; + if ( $calendar_object->status == 'TENTATIVE' ) { + $extra = ';BUSY-TENTATIVE'; + } + 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') ); + foreach( $expansion AS $k => $v ) { + $dtstart = $v->GetProperty('DTSTART'); + $start_date = new RepeatRuleDateTime($dtstart->Value()); + $duration = $v->GetProperty('DURATION'); + $end_date = clone($start_date); + $end_date->modify( $duration->Value() ); + $thisfb = $start_date->UTC() .'/'. $end_date->UTC() . $extra; + array_push( $fbtimes, $thisfb ); + } + } + } + + $freebusy = new iCalComponent(); + $freebusy->SetType('VFREEBUSY'); + $freebusy->AddProperty('DTSTAMP', date('Ymd\THis\Z')); + $freebusy->AddProperty('DTSTART', $range_start->UTC()); + $freebusy->AddProperty('DTEND', $range_end->UTC()); + + sort( $fbtimes ); + foreach( $fbtimes AS $k => $v ) { + $text = explode(';',$v,2); + $freebusy->AddProperty( 'FREEBUSY', $text[0], (isset($text[1]) ? array('FBTYPE' => $text[1]) : null) ); + } + + + $result = new iCalComponent(); + $result->VCalendar(); + $result->AddComponent($freebusy); + + return $result->Render(); +} + diff --git a/testing/tests/regression-suite/0886-REPORT-freebusy.test b/testing/tests/regression-suite/0886-REPORT-freebusy.test new file mode 100644 index 00000000..7a9a8e5e --- /dev/null +++ b/testing/tests/regression-suite/0886-REPORT-freebusy.test @@ -0,0 +1,33 @@ +# +# Request a freebusy report by URL +# +TYPE=REPORT +URL=http://mycaldav/caldav.php/user1/home/ +HEADER=User-Agent: DAViCalTester/public +HEADER=Content-Type: text/xml; charset="UTF-8" +HEAD + +REPLACE=/^DTSTAMP:\d{8}T\d{6}Z\r?$/DTSTAMP:yyyymmddThhmmssZ/ +REPLACE=/^DTSTART:20060930T120000Z\r?$/DTSTART:correct/ +REPLACE=/^DTEND:20070630T115959Z\r?$/DTEND:correct/ + +BEGINDATA + + + + +ENDDATA + + +QUERY +SELECT dav_name "Dav Name", calendar_item.rrule, status, + to_char(calendar_item.dtstart at time zone 'GMT','YYYYMMDD"T"HH24MISS"Z"') AS "a) start", + to_char(calendar_item.dtend at time zone 'GMT','YYYYMMDD"T"HH24MISS"Z"') AS "b)finish" + FROM caldav_data INNER JOIN calendar_item USING(dav_id,user_no,dav_name) + WHERE caldav_data.user_no = 10 + AND rrule_event_overlaps( dtstart, dtend, rrule, '20061001T000000', '20070630T235959') + AND caldav_data.caldav_type IN ( 'VEVENT', 'VFREEBUSY' ) + AND (calendar_item.status != 'CANCELLED' OR calendar_item.status IS NULL) + AND (calendar_item.class != 'PRIVATE' OR calendar_item.class IS NULL) + ORDER BY 2, 3 +ENDQUERY