Merge branch 'master' of git+ssh://repo.or.cz/srv/git/davical

This commit is contained in:
Andrew McMillan 2008-10-24 18:51:28 +13:00
commit 84845b1359
13 changed files with 365 additions and 74 deletions

View File

@ -42,14 +42,26 @@ $c->pg_connect[] = "dbname=davical user=davical_app";
$c->hide_alarm = true;
/**
*default is false
*If true, then TODO requested from someone other than the admmin or owner
* default is false
* If true, then TODO requested from someone other than the admmin or owner
* of a calendar will not get any answer. Often these todo are only relevant
* to the owner, but in some shared calendar situations they might not be in
* which case you should let this default to false.
*/
$c->hide_TODO = true;
/**
* The default is false for backward compatibility
* If true, then calendars accessed via WebDAV will only be readonly. Any
* changes to them must be applied via CalDAV.
*
* You may want to set this to false during your initial setup to make it
* easier for people to PUT whole calendars as part of the conversion of
* their data. After this it is recommended to turn it off so that clients
* which have been misconfigured are readily identifiable.
*/
$c->readonly_webdav_collections = true;
/***************************************************************************
* *
* ADMIN web Interface *

View File

@ -378,3 +378,55 @@ BEGIN
RETURN newname;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER caldav_data_modified ON caldav_data CASCADE;
CREATE or REPLACE FUNCTION caldav_data_modified() RETURNS TRIGGER AS $$
DECLARE
coll_id caldav_data.collection_id%TYPE;
BEGIN
IF TG_OP = 'UPDATE' THEN
IF NEW.caldav_data = OLD.caldav_data AND NEW.collection_id = OLD.collection_id THEN
-- Nothing for us to do
RETURN NEW;
END IF;
END IF;
IF TG_OP = 'DELETE' OR TG_OP = 'UPDATE' THEN
UPDATE collection
SET modified = current_timestamp, dav_etag = md5(OLD.user_no::text||OLD.dav_name||current_timestamp::text)
WHERE collection_id = OLD.collection_id;
IF TG_OP = 'DELETE' THEN
RETURN OLD;
END IF;
END IF;
IF TG_OP = 'INSERT' THEN
UPDATE collection
SET modified = current_timestamp, dav_etag = md5(NEW.user_no::text||NEW.dav_name||current_timestamp::text)
WHERE collection_id = NEW.collection_id;
RETURN NEW;
END IF;
IF NEW.collection_id != OLD.collection_id THEN
UPDATE collection
SET modified = current_timestamp, dav_etag = md5(NEW.user_no::text||NEW.dav_name||current_timestamp::text)
WHERE collection_id = NEW.collection_id;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER caldav_data_modified AFTER INSERT OR UPDATE OR DELETE ON caldav_data
FOR EACH ROW EXECUTE PROCEDURE caldav_data_modified();
/*
DROP TRIGGER collection_modified ON collection CASCADE;
CREATE or REPLACE FUNCTION collection_modified() RETURNS TRIGGER AS $$
DECLARE
BEGIN
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER collection_modified AFTER INSERT OR UPDATE ON collection
FOR EACH ROW EXECUTE PROCEDURE collection_modified();
*/

View File

@ -71,6 +71,7 @@ CREATE TABLE calendar_item (
percent_complete NUMERIC(7,2),
tz_id TEXT REFERENCES time_zone( tz_id ),
status TEXT,
completed TIMESTAMP WITH TIME ZONE,
dav_id INT8 UNIQUE,
collection_id INT8 REFERENCES collection(collection_id) ON UPDATE CASCADE ON DELETE CASCADE DEFERRABLE,

16
dba/patches/1.2.4.sql Normal file
View File

@ -0,0 +1,16 @@
-- This database update provides new tables for the Principal, for
-- a consistent dav_resource which a principal, collection or calendar_item
-- all inherit from.
BEGIN;
SELECT check_db_revision(1,2,3);
-- Add a column to hold the 'COMPLETED' property from the caldav_data
ALTER TABLE calendar_item ADD COLUMN completed TIMESTAMP WITH TIME ZONE;
SELECT new_db_revision(1,2,4, 'Avril' );
COMMIT;
ROLLBACK;

View File

@ -3,24 +3,24 @@
*
* @package rscds
* @subpackage database
* @author Andrew McMillan <andrew@catalyst.net.nz>
* @copyright Catalyst IT Ltd
* @author Andrew McMillan <andrew@mcmillan.net.nz>
* @copyright Morphoss Ltd - http://www.morphoss.com/
* @license http://gnu.org/copyleft/gpl.html GNU GPL v2
*/
-- How many days are there in a particular month?
CREATE or REPLACE FUNCTION rrule_days_in_month( TIMESTAMP WITH TIME ZONE ) RETURNS INT AS '
CREATE or REPLACE FUNCTION rrule_days_in_month( TIMESTAMP WITH TIME ZONE ) RETURNS INT AS $$
DECLARE
in_time ALIAS FOR $1;
days INT;
BEGIN
RETURN date_part( ''days'', date_trunc( ''month'', in_time + interval ''1 month'') - interval ''1 day'' );
RETURN date_part( 'days', date_trunc( 'month', in_time + interval '1 month') - interval '1 day' );
END;
' LANGUAGE 'plpgsql' IMMUTABLE STRICT;
$$ LANGUAGE 'plpgsql' IMMUTABLE STRICT;
-- Return a SETOF text strings, split on the commas in the original one
CREATE or REPLACE FUNCTION rrule_split_on_commas( TEXT ) RETURNS SETOF TEXT AS '
CREATE or REPLACE FUNCTION rrule_split_on_commas( TEXT ) RETURNS SETOF TEXT AS $$
DECLARE
in_text ALIAS FOR $1;
part TEXT;
@ -29,7 +29,7 @@ DECLARE
BEGIN
remainder := in_text;
LOOP
cpos := position( '','' in remainder );
cpos := position( ',' in remainder );
IF cpos = 0 THEN
part := remainder;
EXIT;
@ -42,10 +42,10 @@ BEGIN
RETURN NEXT part;
RETURN;
END;
' LANGUAGE 'plpgsql' IMMUTABLE STRICT;
$$ LANGUAGE 'plpgsql' IMMUTABLE STRICT;
-- Return a SETOF dates within the month of a particular date which match a single BYDAY rule specification
CREATE or REPLACE FUNCTION rrule_month_bydayrule_set( TIMESTAMP WITH TIME ZONE, TEXT ) RETURNS SETOF TIMESTAMP WITH TIME ZONE AS '
CREATE or REPLACE FUNCTION rrule_month_bydayrule_set( TIMESTAMP WITH TIME ZONE, TEXT ) RETURNS SETOF TIMESTAMP WITH TIME ZONE AS $$
DECLARE
in_time ALIAS FOR $1;
bydayrule ALIAS FOR $2;
@ -55,54 +55,54 @@ DECLARE
each_day TIMESTAMP WITH TIME ZONE;
this_month INT;
BEGIN
dow := position(substring( bydayrule from ''..$'') in ''SUMOTUWETHFRSA'') / 2;
each_day := date_trunc( ''month'', in_time ) + (in_time::time)::interval;
this_month := date_part( ''month'', in_time );
first_dow := date_part( ''dow'', each_day );
each_day := each_day - ( first_dow::text || ''days'')::interval
+ ( dow::text || ''days'')::interval
+ CASE WHEN dow > first_dow THEN ''1 week''::interval ELSE ''0s''::interval END;
dow := position(substring( bydayrule from '..$') in 'SUMOTUWETHFRSA') / 2;
each_day := date_trunc( 'month', in_time ) + (in_time::time)::interval;
this_month := date_part( 'month', in_time );
first_dow := date_part( 'dow', each_day );
each_day := each_day - ( first_dow::text || 'days')::interval
+ ( dow::text || 'days')::interval
+ CASE WHEN dow > first_dow THEN '1 week'::interval ELSE '0s'::interval END;
IF length(bydayrule) > 2 THEN
index := (substring(bydayrule from ''^[0-9-]+''))::int;
index := (substring(bydayrule from '^[0-9-]+'))::int;
-- Possibly we should check that (index != 0) here, which is an error
IF index = 0 THEN
RAISE NOTICE ''Ignored invalid BYDAY rule part "%".'', bydayrule;
RAISE NOTICE 'Ignored invalid BYDAY rule part "%".', bydayrule;
ELSIF index > 0 THEN
-- The simplest case, such as 2MO for the second monday
each_day := each_day + ((index - 1)::text || '' weeks'')::interval;
each_day := each_day + ((index - 1)::text || ' weeks')::interval;
ELSE
each_day := each_day + ''5 weeks''::interval;
WHILE date_part(''month'', each_day) != this_month LOOP
each_day := each_day - ''1 week''::interval;
each_day := each_day + '5 weeks'::interval;
WHILE date_part('month', each_day) != this_month LOOP
each_day := each_day - '1 week'::interval;
END LOOP;
-- Note that since index is negative, (-2 + 1) == -1, for example
each_day := each_day + ( (index + 1)::text || '' weeks'')::interval ;
each_day := each_day + ( (index + 1)::text || ' weeks')::interval ;
END IF;
-- Sometimes (e.g. 5TU or -5WE) there might be no such date in some months
IF date_part(''month'', each_day) = this_month THEN
IF date_part('month', each_day) = this_month THEN
RETURN NEXT each_day;
END IF;
ELSE
-- Return all such days that are within the given month
WHILE date_part(''month'', each_day) = this_month LOOP
WHILE date_part('month', each_day) = this_month LOOP
RETURN NEXT each_day;
each_day := each_day + ''1 week''::interval;
each_day := each_day + '1 week'::interval;
END LOOP;
END IF;
RETURN;
END;
' LANGUAGE 'plpgsql' IMMUTABLE STRICT;
$$ LANGUAGE 'plpgsql' IMMUTABLE STRICT;
-- Return a SETOF dates within the month of a particular date which match a string of BYDAY rule specifications
CREATE or REPLACE FUNCTION rrule_month_byday_set( TIMESTAMP WITH TIME ZONE, TEXT ) RETURNS SETOF TIMESTAMP WITH TIME ZONE AS '
CREATE or REPLACE FUNCTION rrule_month_byday_set( TIMESTAMP WITH TIME ZONE, TEXT ) RETURNS SETOF TIMESTAMP WITH TIME ZONE AS $$
DECLARE
in_time ALIAS FOR $1;
byday ALIAS FOR $2;
@ -119,13 +119,13 @@ BEGIN
RETURN;
END;
' LANGUAGE 'plpgsql' IMMUTABLE STRICT;
$$ LANGUAGE 'plpgsql' IMMUTABLE STRICT;
-- Return a SETOF dates within the month of a particular date which match a string of BYDAY rule specifications
CREATE or REPLACE FUNCTION rrule_month_bymonthday_set( TIMESTAMP WITH TIME ZONE, TEXT ) RETURNS SETOF TIMESTAMP WITH TIME ZONE AS '
CREATE or REPLACE FUNCTION rrule_month_bymonthday_set( TIMESTAMP WITH TIME ZONE, TEXT ) RETURNS SETOF TIMESTAMP WITH TIME ZONE AS $$
DECLARE
in_time ALIAS FOR $1;
bymonthday ALIAS FOR $2;
@ -136,21 +136,21 @@ DECLARE
BEGIN
daysinmonth := rrule_days_in_month(in_time);
month_start := date_trunc( ''month'', in_time ) + (in_time::time)::interval;
month_start := date_trunc( 'month', in_time ) + (in_time::time)::interval;
FOR dayrule IN SELECT * FROM rrule_split_on_commas( bymonthday ) LOOP
dayoffset := dayrule.rrule_split_on_commas::int;
IF dayoffset = 0 THEN
RAISE NOTICE ''Ignored invalid BYMONTHDAY part "%".'', dayrule.rrule_split_on_commas;
RAISE NOTICE 'Ignored invalid BYMONTHDAY part "%".', dayrule.rrule_split_on_commas;
dayoffset := 0;
ELSIF dayoffset > daysinmonth THEN
dayoffset := 0;
ELSIF dayoffset < (-1 * daysinmonth) THEN
dayoffset := 0;
ELSIF dayoffset > 0 THEN
RETURN NEXT month_start + ((dayoffset - 1)::text || ''days'')::interval;
RETURN NEXT month_start + ((dayoffset - 1)::text || 'days')::interval;
ELSE
RETURN NEXT month_start + ((daysinmonth + dayoffset)::text || ''days'')::interval;
RETURN NEXT month_start + ((daysinmonth + dayoffset)::text || 'days')::interval;
END IF;
END LOOP;
@ -158,12 +158,12 @@ BEGIN
RETURN;
END;
' LANGUAGE 'plpgsql' IMMUTABLE STRICT;
$$ LANGUAGE 'plpgsql' IMMUTABLE STRICT;
-- Return a SETOF dates within the week of a particular date which match a single BYDAY rule specification
CREATE or REPLACE FUNCTION rrule_week_byday_set( TIMESTAMP WITH TIME ZONE, TEXT ) RETURNS SETOF TIMESTAMP WITH TIME ZONE AS '
CREATE or REPLACE FUNCTION rrule_week_byday_set( TIMESTAMP WITH TIME ZONE, TEXT ) RETURNS SETOF TIMESTAMP WITH TIME ZONE AS $$
DECLARE
in_time ALIAS FOR $1;
byweekday ALIAS FOR $2;
@ -171,21 +171,21 @@ DECLARE
dow INT;
our_day TIMESTAMP WITH TIME ZONE;
BEGIN
our_day := date_trunc( ''week'', in_time ) + (in_time::time)::interval;
our_day := date_trunc( 'week', in_time ) + (in_time::time)::interval;
FOR dayrule IN SELECT * FROM rrule_split_on_commas( byweekday ) LOOP
dow := position(substring( dayrule.rrule_split_on_commas from ''..$'') in ''SUMOTUWETHFRSA'') / 2;
RETURN NEXT our_day + ((dow - 1)::text || ''days'')::interval;
dow := position(substring( dayrule.rrule_split_on_commas from '..$') in 'SUMOTUWETHFRSA') / 2;
RETURN NEXT our_day + ((dow - 1)::text || 'days')::interval;
END LOOP;
RETURN;
END;
' LANGUAGE 'plpgsql' IMMUTABLE STRICT;
$$ LANGUAGE 'plpgsql' IMMUTABLE STRICT;
CREATE or REPLACE FUNCTION event_has_exceptions( TEXT ) RETURNS BOOLEAN AS '
SELECT $1 ~ ''\nRECURRENCE-ID(;TZID=[^:]+)?:[[:space:]]*[[:digit:]]{8}(T[[:digit:]]{6})?''
' LANGUAGE 'sql' IMMUTABLE STRICT;
CREATE or REPLACE FUNCTION event_has_exceptions( TEXT ) RETURNS BOOLEAN AS $$
SELECT $1 ~ E'\nRECURRENCE-ID(;TZID=[^:]+)?:[[:space:]]*[[:digit:]]{8}(T[[:digit:]]{6})?'
$$ LANGUAGE 'sql' IMMUTABLE STRICT;

View File

@ -41,6 +41,7 @@ switch ( $request->method ) {
case 'MKCALENDAR': include_once("caldav-MKCALENDAR.php"); break;
case 'MKCOL': include_once("caldav-MKCALENDAR.php"); break;
case 'PUT': include_once("caldav-PUT.php"); break;
case 'POST': include_once("caldav-POST.php"); break;
case 'GET': include_once("caldav-GET.php"); break;
case 'HEAD': include_once("caldav-GET.php"); break;
case 'DELETE': include_once("caldav-DELETE.php"); break;

View File

@ -93,6 +93,13 @@ class CalDAVPrincipal
else if ( isset($parameters['user_no']) ) {
$usr = getUserByID($parameters['user_no']);
}
else if ( isset($parameters['email']) && isset($parameters['options']['allow_by_email']) ) {
$parameters['options']['allow_by_email'] = false;
if ( $username = $this->UsernameFromEMail($parameters['email']) ) {
$usr = getUserByName($username);
$this->by_email = true;
}
}
else if ( isset($parameters['path']) ) {
dbg_error_log( "principal", "Finding Principal from path: '%s', options.allow_by_email: '%s'", $parameters['path'], $parameters['options']['allow_by_email'] );
if ( $username = $this->UsernameFromPath($parameters['path'], $parameters['options']) ) {
@ -209,6 +216,21 @@ class CalDAVPrincipal
}
/**
* Work out the username, based on the given e-mail
* @param string $email The email address to be used.
*/
function UsernameFromEMail( $email ) {
$qry = new PgQuery("SELECT user_no, username FROM usr WHERE email = ?;", $email );
if ( $qry->Exec("principal") && $user = $qry->Fetch() ) {
$user_no = $user->user_no;
$username = $user->username;
}
return $username;
}
/**
* Returns the array of privilege names converted into XMLElements
*/

View File

@ -64,6 +64,12 @@ class CalDAVRequest
*/
var $collection_path;
/**
* The type of collection being requested:
* calendar, schedule-inbox, schedule-outbox
*/
var $collection_type;
/**
* Create a new CalDAVRequest object.
*/
@ -196,22 +202,44 @@ class CalDAVRequest
/**
* Get the ID of the collection we are referring to
*/
if ( !isset($this->collection_id) && preg_match( '#^(/.+/.+/)[^/]*$#', $this->path, $matches ) ) {
$qry = new PgQuery( "SELECT collection_id FROM collection WHERE user_no = ? AND dav_name = ?;", $this->user_no, $matches[1] );
if ( $qry->Exec('caldav') && $qry->rows == 1 && ($row = $qry->Fetch()) ) {
$this->collection_id = $row->collection_id;
$this->collection_path = $matches[1];
if ( preg_match( '#^(/.+/.+/)[^/]*$#', $this->path, $matches ) ) {
$this->collection_type = 'calendar';
if ( preg_match( '#^(/[^/]+/\.(in|out)/)[^/]*$#', $this->path, $matches ) ) {
$this->collection_type = $matches[2];
}
if ( ! isset($this->collection_id) ) {
$qry = new PgQuery( "SELECT collection_id FROM collection WHERE user_no = ? AND dav_name = ?;",
($this->collection_type == 'calendar' ? $this->user_no : $session->user_no), $matches[1] );
if ( $qry->Exec('caldav') && $qry->rows == 1 && ($row = $qry->Fetch()) ) {
$this->collection_id = $row->collection_id;
$this->collection_path = $matches[1];
}
else if ( preg_match( '#^((/[^/]+/)\.(in|out)/)[^/]*$#', $this->path, $matches ) ) {
// Request is for a scheduling inbox or outbox (or something inside one) and we should auto-create it
$displayname = $session->fullname . ($matches[3] == 'in' ? ' Inbox' : ' Outbox');
$sql = <<<EOSQL
INSERT INTO collection ( user_no, parent_container, dav_name, dav_displayname, is_calendar, created, modified )
VALUES( ?, ?, ?, ?, FALSE, current_timestamp, current_timestamp )
EOSQL;
$qry = new PgQuery( $sql, $session->user_no, $matches[2] , $matches[1], $displayname );
$qry->Exec('caldav');
dbg_error_log( "caldav", "Created new collection as '$displayname'." );
$qry = new PgQuery( "SELECT collection_id FROM collection WHERE user_no = ? AND dav_name = ?;", $session->user_no, $matches[1] );
if ( $qry->Exec('caldav') && $qry->rows == 1 && ($row = $qry->Fetch()) ) {
$this->collection_id = $row->collection_id;
$this->collection_path = $matches[1];
}
}
}
}
dbg_error_log( "caldav", " Collection '%s' is %d", $this->collection_path, $this->collection_id );
dbg_error_log( "caldav", " Collection '%s' is %d, type %s", $this->collection_path, $this->collection_id, $this->collection_type );
/**
* Evaluate our permissions for accessing the target
*/
$this->setPermissions();
/**
* If the content we are receiving is XML then we parse it here. RFC2518 says we
* should reasonably expect to see either text/xml or application/xml
@ -236,9 +264,9 @@ class CalDAVRequest
$this->etag_if_match = str_replace('"','',$_SERVER["HTTP_IF_MATCH"]);
if ( $this->etag_if_match == '' ) unset($this->etag_if_match);
}
}
/**
* Work out the user whose calendar we are accessing, based on elements of the path.
*/
@ -532,15 +560,30 @@ class CalDAVRequest
function AllowedTo( $activity ) {
if ( isset($this->permissions['all']) ) return true;
switch( $activity ) {
case "CALDAV:schedule-send-freebusy":
return isset($this->permissions['read']) || isset($this->permissions['freebusy']);
break;
case "CALDAV:schedule-send-invite":
return isset($this->permissions['read']) || isset($this->permissions['freebusy']);
break;
case "CALDAV:schedule-send-reply":
return isset($this->permissions['read']) || isset($this->permissions['freebusy']);
break;
case 'freebusy':
return isset($this->permissions['read']) || isset($this->permissions['freebusy']);
break;
case 'delete':
return isset($this->permissions['write']) || isset($this->permissions['unbind']);
break;
case 'proppatch':
return isset($this->permissions['write']) || isset($this->permissions['write-properties']);
break;
case 'modify':
return isset($this->permissions['write']) || isset($this->permissions['write-content']);
break;

View File

@ -252,7 +252,7 @@ class iCalDate {
* Add some integer number of days to a date
*/
function AddDays( $dd ) {
dbg_error_log( "RRule", " Adding %d days to %s", $dd, $this->_text );
$at_start = $this->_text;
$this->_dd += $dd;
while ( 1 > $this->_dd ) {
$this->_mo--;
@ -272,7 +272,7 @@ class iCalDate {
}
$this->_EpochFromParts();
$this->_TextFromEpoch();
dbg_error_log( "RRule", " Added %d days and got %s", $dd, $this->_text );
dbg_error_log( "RRule", " Added %d days to %s and got %s", $dd, $at_start, $this->_text );
}
@ -436,20 +436,23 @@ class iCalDate {
/**
* Applies any BYDAY to the week to return a set of days
* @param string $byday The BYDAY rule
* @param string $increasing When we are moving by months, we want any day of the week, but when by day we only want to increase. Default false.
* @return array An array of the day numbers for the week which meet the rule.
*/
function GetWeekByDay($byday) {
function GetWeekByDay($byday, $increasing = false) {
global $ical_weekdays;
dbg_error_log( "RRule", " Applying BYDAY %s to week", $byday );
$days = split(',',$byday);
$dow = date('w',$this->_epoch);
$set = array();
foreach( $days AS $k => $v ) {
$daynum = $ical_weekdays[$v];
$dd = $this->_dd - $dow + $daynum;
if ( $daynum < $this->_wkst ) $dd += 7;
$set[$dd] = $dd;
if ( $dd > $this->_dd || !$increasing ) $set[$dd] = $dd;
}
asort( $set, SORT_NUMERIC );
return $set;
}
@ -893,15 +896,15 @@ class RRule {
$limit = 100;
do {
$limit--;
if ( $this->_started ) {
$next->AddDays($this->_part['INTERVAL']);
}
else {
$this->_started = true;
}
if ( $this->_started ) {
$next->AddDays($this->_part['INTERVAL']);
}
else {
$this->_started = true;
}
if ( isset($this->_part['BYDAY']) ) {
$days = $next->GetWeekByDay($this->_part['BYDAY']);
$days = $next->GetWeekByDay($this->_part['BYDAY'], true );
}
else
$days[$next->_dd] = $next->_dd;

View File

@ -10,12 +10,15 @@
*/
dbg_error_log("POST", "method handler");
require_once("XMLDocument.php");
require_once("iCalendar.php");
include_once("RRule.php");
if ( ! $request->AllowedTo("CALDAV:schedule-send-freebusy")
&& ! $request->AllowedTo("CALDAV:schedule-send-invite")
&& ! $request->AllowedTo("CALDAV:schedule-send-reply") ) {
$request->DoResponse(403);
// $request->DoResponse(403);
dbg_error_log( "WARN", ": POST: permissions not yet checked" );
}
if ( ! ini_get('open_basedir') && (isset($c->dbg['ALL']) || $c->dbg['post']) ) {
@ -27,11 +30,140 @@ if ( ! ini_get('open_basedir') && (isset($c->dbg['ALL']) || $c->dbg['post']) ) {
}
function handle_freebusy_request( $ic ) {
global $c, $session, $request;
$reply = new XMLDocument( array("DAV:" => "", "urn:ietf:params:xml:ns:caldav" => "C" ) );
$responses = array();
$fbq_start = $ic->Get('DTSTART');
$fbq_end = $ic->Get('DTEND');
$component =& $ic->component->FirstNonTimezone();
$attendees = $component->GetProperties('ATTENDEE');
$fb_response_template = new iCalendar( array( 'DTSTAMP' => date('Ymd\THis\Z'),
'DTSTART' => $fbq_start,
'DTEND' => $fbq_end,
'UID' => $ic->Get('UID'),
'ORGANIZER' => $ic->Get('ORGANIZER'),
'type' => 'VFREEBUSY' ) );
$fb_response_template->component->AddProperty( new iCalProp("METHOD:REPLY") );
foreach( $attendees AS $k => $attendee ) {
$attendee_email = preg_replace( '/^mailto:/', '', $attendee->Value() );
if ( ! ( isset($fbq_start) || isset($fbq_end) ) ) {
$request->DoResponse( 400, 'All valid freebusy requests MUST contain a DTSTART and a DTEND' );
}
$where = " WHERE usr.email = ? AND collection.is_calendar ";
if ( isset( $fbq_start ) ) {
$where .= "AND (dtend >= ".qpg($fbq_start)."::timestamp with time zone ";
$where .= "OR calculate_later_timestamp(".qpg($fbq_start)."::timestamp with time zone,dtend,rrule) >= ".qpg($fbq_start)."::timestamp with time zone) ";
}
if ( isset( $fbq_end ) ) {
$where .= "AND dtstart <= ".qpg($fbq_end)."::timestamp with time zone ";
}
$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) ";
/**
* @TODO Some significant permissions need to be added around the visibility of free/busy
* but lets get it working first...
*/
$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 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 PgQuery( $sql, $attendee_email );
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;
$fbparams = array( "FBTYPE" => "BUSY-TENTATIVE" );
$fb = clone($fb_response_template);
$fb->Add( $attendee->Name(), $attendee->Value(), $attendee->parameters );
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->Add("FREEBUSY", sprintf("%s/%s", $date->Render('Ymd\THis'), $todate->Render('Ymd\THis') ), $fbparams );
}
}
else {
$fb->Add("FREEBUSY;FBTYPE=BUSY-TENTATIVE",sprintf("%s/%s", $start->Render('Ymd\THis'), $v->finish ), $fbparams );
}
}
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->Add("FREEBUSY", sprintf("%s/%s", $date->Render('Ymd\THis'), $todate->Render('Ymd\THis') ) );
}
}
else {
$fb->Add("FREEBUSY", sprintf("%s/%s", $start->Render('Ymd\THis'), $v->finish ) );
}
}
$response = new XMLElement( $reply->Caldav("response") );
$response->NewElement( $reply->Caldav("recipient"), new XMLElement("href",$_SERVER['HTTP_RECIPIENT']) );
$response->NewElement( $reply->Caldav("request-status"), "2.0;Success" ); // Cargo-cult setting
$response->NewElement( $reply->Caldav("calendar-data"), $fb->Render() );
$responses[] = $response;
}
$response = new XMLElement( "schedule-response", $responses, $reply->GetXmlNsArray() );
$request->DoResponse( 200, $response->Render() );
}
$ical = new iCalendar( array('icalendar' => $request->raw_post) );
switch ( $ical->properties['METHOD'] ) {
$calendar_properties = $ical->component->GetProperties('METHOD');
$method = $calendar_properties[0]->Value();
switch ( $method ) {
case 'REQUEST':
dbg_error_log("POST", ": Handling iTIP 'REQUEST' method.", $method );
handle_freebusy_request( $ical );
break;
default:
dbg_error_log("POST", ": Unhandled '%s' method in request.", $ical->properties['METHOD'] );
}
dbg_error_log("POST", ": Unhandled '%s' method in request.", $method );
}

View File

@ -427,8 +427,6 @@ function item_to_xml( $item ) {
$allprop = isset($prop_list['DAV::allprop']);
$item->properties = get_arbitrary_properties($item->dav_name);
$url = ConstructURL($item->dav_name);
$prop = new XMLElement("prop");
@ -464,6 +462,8 @@ function item_to_xml( $item ) {
*/
add_general_properties( $prop, $not_found, $denied, $item );
add_arbitrary_properties($prop, $not_found, $item );
return build_propstat_response( $prop, $not_found, $denied, $url );
}

View File

@ -124,6 +124,15 @@ function calendar_to_xml( $properties, $item ) {
break;
case 'resourcetype':
$prop->NewElement($k, new XMLElement($reply->Caldav("calendar"), false) );
if ( $request->collection_type == 'in' ) {
$prop->NewElement($k, new XMLElement($reply->Caldav("schedule-inbox"), false) );
}
else if ( $request->collection_type == 'out' ) {
$prop->NewElement($k, new XMLElement($reply->Caldav("schedule-outbox"), false) );
}
else {
$prop->NewElement($k, new XMLElement($reply->Caldav("schedule-calendar"), false) );
}
break;
case 'displayname':
$prop->NewElement($k, $displayname );
@ -171,6 +180,6 @@ elseif ( $xmltree->GetTag() == "urn:ietf:params:xml:ns:caldav:calendar-multiget"
include("caldav-REPORT-multiget.php");
}
else {
$request->DoResponse( 501, "XML is not a supported REPORT query document" );
$request->DoResponse( 501, "The XML is not a supported REPORT query document" );
}

View File

@ -18,6 +18,7 @@ $tests = array(
, "20061117T073000" => "RRULE:FREQ=MONTHLY;BYDAY=1MO,2WE,3FR,-1SU"
, "20061107T103000" => "RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR"
, "20061107T113000" => "RRULE:FREQ=MONTHLY;BYDAY=MO,TU,WE,TH,FR;BYSETPOS=-1"
, "20081020T110000" => "RRULE:FREQ=DAILY;INTERVAL=1;BYDAY=MO,TU,WE,TH,FR"
);
foreach( $tests AS $start => $rrule ) {
@ -26,7 +27,7 @@ foreach( $tests AS $start => $rrule ) {
$rule = new RRule( new iCalDate($start), $rrule );
$i = 0;
do {
if ( ($i % 4) == 0 ) echo "\n";
if ( ($i % 10) == 0 ) echo "\n";
$date = $rule->GetNext();
if ( isset($date) ) {
echo " " . $date->Render();
@ -38,4 +39,3 @@ foreach( $tests AS $start => $rrule ) {
}
exit(0);
?>