mirror of
https://gitlab.com/davical-project/davical.git
synced 2026-05-24 02:24:39 +00:00
Merge branch 'master' of git+ssh://repo.or.cz/srv/git/davical
This commit is contained in:
commit
84845b1359
@ -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 *
|
||||
|
||||
@ -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();
|
||||
*/
|
||||
@ -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
16
dba/patches/1.2.4.sql
Normal 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;
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 );
|
||||
}
|
||||
|
||||
@ -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 );
|
||||
}
|
||||
|
||||
|
||||
@ -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" );
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
?>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user