Refactoring free/busy handling to a single core routine with RRule-2

This commit is contained in:
Andrew McMillan 2010-08-30 09:08:17 +12:00
parent 64f20edaab
commit fa67ef987e
4 changed files with 172 additions and 137 deletions

View File

@ -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;
}

View File

@ -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

View File

@ -0,0 +1,78 @@
<?php
/**
* Function to include which handles building a free/busy response to
* be used in either the REPORT, response to a POST, or response to a
* a freebusy GET request.
*/
include_once("RRule-v2.php");
function get_freebusy( $path_match, $range_start, $range_end ) {
global $request;
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() );
$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();
}

View File

@ -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
<?xml version="1.0" encoding="UTF-8"?>
<free-busy-query xmlns:D="DAV:" xmlns="urn:ietf:params:xml:ns:caldav">
<time-range start="20060930T120000Z" end="20070630T115959Z"/>
</free-busy-query>
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