From 2b8b67ab0bf45d3b086ab3e84b3eb93518d84a24 Mon Sep 17 00:00:00 2001 From: Andrew McMillan Date: Sun, 24 Sep 2006 17:08:39 +1200 Subject: [PATCH] Kind of working now, with either Lighting or Evolution. --- dba/caldav_functions.sql | 190 +++++++++++++++++++++++++++++++++++++++ dba/rscds.sql | 72 +++++++++++++-- inc/caldav-DELETE.php | 12 ++- inc/caldav-GET.php | 10 +-- inc/caldav-OPTIONS.php | 6 +- inc/caldav-PUT.php | 39 ++++---- inc/caldav-REPORT.php | 63 ++++++++++--- inc/vEvent.php | 161 +++++++++++++++++++++++++++++---- rscds.webprj | 1 + 9 files changed, 494 insertions(+), 60 deletions(-) create mode 100644 dba/caldav_functions.sql diff --git a/dba/caldav_functions.sql b/dba/caldav_functions.sql new file mode 100644 index 00000000..c6932a45 --- /dev/null +++ b/dba/caldav_functions.sql @@ -0,0 +1,190 @@ +-- Functions for CalDAV handling + +CREATE or REPLACE FUNCTION apply_month_byday( TIMESTAMP WITH TIME ZONE, TEXT ) RETURNS TIMESTAMP WITH TIME ZONE AS ' +DECLARE + in_time ALIAS FOR $1; + byday ALIAS FOR $2; + weeks INT; + dow INT; + temp_txt TEXT; + dd INT; + mm INT; + yy INT; + our_dow INT; + our_answer TIMESTAMP WITH TIME ZONE; +BEGIN + dow := position(substring( byday from ''..$'') in ''SUMOTUWETHFRSA'') / 2; + temp_txt := substring(byday from ''([0-9]+)''); + weeks := temp_txt::int; + + -- RAISE NOTICE ''DOW: %, Weeks: %(%s)'', dow, weeks, temp_txt; + + IF substring(byday for 1) = ''-'' THEN + -- Last XX of month, or possibly second-to-last, but unlikely + mm := extract( ''month'' from in_time); + yy := extract( ''year'' from in_time); + + -- Start with the last day of the month + our_answer := (yy::text || ''-'' || (mm+1)::text || ''-01'')::timestamp - ''1 day''::interval; + dd := extract( ''dow'' from our_answer); + dd := dd - dow; + IF dd < 0 THEN + dd := dd + 7; + END IF; + + -- Having calculated the right day of the month, we now apply that back to in_time + -- which contains the otherwise-unobtainable timezone detail (and the time) + our_answer = our_answer - (dd::text || ''days'')::interval; + dd := extract( ''day'' from our_answer) - extract( ''day'' from in_time); + our_answer := in_time + (dd::text || ''days'')::interval; + + IF weeks > 1 THEN + weeks := weeks - 1; + our_answer := our_answer - (weeks::text || ''weeks'')::interval; + END IF; + + ELSE + + -- Shift our date to the correct day of week.. + our_dow := extract( ''dow'' from in_time); + our_dow := our_dow - dow; + dd := extract( ''day'' from in_time); + IF our_dow >= dd THEN + our_dow := our_dow - 7; + END IF; + our_answer := in_time - (our_dow::text || ''days'')::interval; + dd = extract( ''day'' from our_answer); + + -- Shift the date to the correct week... + dd := weeks - ((dd+6) / 7); + IF dd != 0 THEN + our_answer := our_answer + ((dd::text || ''weeks'')::interval); + END IF; + + END IF; + + RETURN our_answer; + +END; +' LANGUAGE 'plpgsql' IMMUTABLE STRICT; + + +CREATE or REPLACE FUNCTION calculate_later_timestamp( TIMESTAMP WITH TIME ZONE, TIMESTAMP WITH TIME ZONE, TEXT ) RETURNS TIMESTAMP WITH TIME ZONE AS ' +DECLARE + earliest ALIAS FOR $1; + basedate ALIAS FOR $2; + repeatrule ALIAS FOR $3; + frequency TEXT; + temp_txt TEXT; + length INT; + count INT; + byday TEXT; + bymonthday INT; + basediff INTERVAL; + past_repeats INT8; + units TEXT; + dow TEXT; + our_answer TIMESTAMP WITH TIME ZONE; + loopcount INT; +BEGIN + temp_txt := substring(repeatrule from ''UNTIL=([0-9TZ]+)(;|$)''); + IF temp_txt::timestamp with time zone < earliest THEN + RETURN NULL; + END IF; + + frequency := substring(repeatrule from ''FREQ=([A-Z]+)(;|$)''); + temp_txt := substring(repeatrule from ''INTERVAL=([0-9]+)(;|$)''); + length := temp_txt::int; + basediff := earliest - basedate; + + -- RAISE NOTICE ''Frequency: %, Length: %(%), Basediff: %'', frequency, length, temp_txt, basediff; + + -- Calculate the number of past periods between our base date and our earliest date + IF frequency = ''WEEKLY'' OR frequency = ''DAILY'' THEN + past_repeats := extract(''epoch'' from basediff)::INT8 / 86400; + -- RAISE NOTICE ''Days: %'', past_repeats; + IF frequency = ''WEEKLY'' THEN + past_repeats := past_repeats / 7; + END IF; + ELSE + past_repeats = extract( ''years'' from basediff ); + IF frequency = ''MONTHLY'' THEN + past_repeats = (past_repeats *12) + extract( ''months'' from basediff ); + END IF; + END IF; + past_repeats = (past_repeats / length) + 1; + + -- Check that we have not exceeded the COUNT= limit + temp_txt := substring(repeatrule from ''COUNT=([0-9]+)(;|$)''); + count := temp_txt::int; + -- RAISE NOTICE ''Periods: %, Count: %(%)'', past_repeats, count, temp_txt; + IF ( count <= past_repeats ) THEN + RETURN NULL; + END IF; + + temp_txt := substring(repeatrule from ''BYSETPOS=([0-9-]+)(;|$)''); + byday := substring(repeatrule from ''BYDAY=([0-9A-Z,]+-)(;|$)''); + IF byday IS NOT NULL AND frequency = ''MONTHLY'' THEN + -- Since this could move the date around a month we go back one + -- period just to be extra sure. + past_repeats = past_repeats - 1; + + IF temp_txt IS NOT NULL THEN + -- Crudely hack the BYSETPOS onto the front of BYDAY. While this + -- is not as per rfc2445, RRULE syntax is so complex and overblown + -- that nobody correctly uses comma-separated BYDAY or BYSETPOS, and + -- certainly not within a MONTHLY RRULE. + byday := temp_txt || byday; + END IF; + END IF; + + past_repeats = past_repeats * length; + + units := CASE + WHEN frequency = ''DAILY'' THEN ''days'' + WHEN frequency = ''WEEKLY'' THEN ''weeks'' + WHEN frequency = ''MONTHLY'' THEN ''months'' + WHEN frequency = ''YEARLY'' THEN ''years'' + END; + + temp_txt := substring(repeatrule from ''BYMONTHDAY=([0-9,]+)(;|$)''); + bymonthday := temp_txt::int; + + -- With all of the above calculation, this date should be close to (but less than) + -- the target, and we should only loop once or twice. + our_answer := basedate + (past_repeats::text || units)::interval; + + loopcount := 1000; -- Not really needed, but stops an infinite loop if there is a bug! + LOOP + -- RAISE NOTICE ''Testing date: %'', our_answer; + IF frequency = ''WEEKLY'' THEN + -- Weekly repeats are only on specific days + -- I think this is not really right, since a WEEKLY on MO,WE,FR should + -- occur three times each week and this will only be once a week. + dow = substring( to_char( our_answer, ''DY'' ) for 2); + CONTINUE WHEN position( dow in byday ) = 0; + ELSIF frequency = ''MONTHLY'' AND byday IS NOT NULL THEN + -- This works fine, except that maybe there are multiple BYDAY + -- components. e.g. 1TU,3TU might be 1st & 3rd tuesdays. + our_answer := apply_month_byday( our_answer, byday ); + ELSIF bymonthday IS NOT NULL AND frequency = ''MONTHLY'' AND bymonthday < 1 THEN + -- We do not deal with this situation at present + RAISE NOTICE ''The case of negative BYMONTHDAY is not handled yet.''; + END IF; + + EXIT WHEN our_answer >= earliest; + + loopcount := loopcount - 1; + IF loopcount < 0 THEN + RAISE EXCEPTION ''Could not cope with dates after % using % from %'', earliest, repeatrule, basedate; + RETURN NULL; + END IF; + + -- Increment for our next time through the loop... + our_answer := our_answer + (length::text || units)::interval; + END LOOP; + + RETURN our_answer; + +END; +' LANGUAGE 'plpgsql' IMMUTABLE STRICT; diff --git a/dba/rscds.sql b/dba/rscds.sql index 9ff92993..c88afe0e 100644 --- a/dba/rscds.sql +++ b/dba/rscds.sql @@ -14,10 +14,10 @@ CREATE TABLE caldav_data ( caldav_type TEXT, logged_user INT references usr(user_no), - PRIMARY KEY ( user_no, vevent_name, vevent_etag ) + PRIMARY KEY ( user_no, dav_name ) ); -GRANT SELECT,INSERT,UPDATE,DELETE ON vevent_data TO general; +GRANT SELECT,INSERT,UPDATE,DELETE ON caldav_data TO general; -- Not particularly needed, perhaps, except as a way to collect -- a bunch of valid iCalendar time zone specifications... :-) @@ -31,30 +31,84 @@ GRANT SELECT,INSERT ON time_zone TO general; -- The parsed event. Here we have pulled those events apart somewhat. CREATE TABLE event ( user_no INT references usr(user_no), - vevent_name TEXT, - vevent_etag TEXT, + dav_name TEXT, + dav_etag TEXT, -- Extracted vEvent event data uid TEXT, - dtstamp TEXT, + created TIMESTAMP, + last_modified TIMESTAMP, + dtstamp TIMESTAMP, dtstart TIMESTAMP WITH TIME ZONE, dtend TIMESTAMP WITH TIME ZONE, + due TIMESTAMP WITH TIME ZONE, summary TEXT, location TEXT, + description TEXT, + priority INT, class TEXT, transp TEXT, - description TEXT, rrule TEXT, + url TEXT, + percent_complete NUMERIC(7,2), tz_id TEXT REFERENCES time_zone( tz_id ), - -- Cascade updates / deletes from the vevent_data table - CONSTRAINT vevent_exists FOREIGN KEY ( user_no, vevent_name, vevent_etag ) - REFERENCES vevent_data ( user_no, vevent_name, vevent_etag ) + -- Cascade updates / deletes from the caldav_data table + CONSTRAINT caldav_exists FOREIGN KEY ( user_no, dav_name ) + REFERENCES caldav_data ( user_no, dav_name ) MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE ); GRANT SELECT,INSERT,UPDATE,DELETE ON event TO general; +-- BEGIN:VTODO +-- CREATED:20060921T035148Z +-- LAST-MODIFIED:20060921T035301Z +-- DTSTAMP:20060921T035301Z +-- UID:9a495928-276c-406b-8acd-e0883dfe68e3 +-- SUMMARY:Something to do +-- PRIORITY:0 +-- CLASS:PUBLIC +-- DUE;TZID=/mozilla.org/20050126_1/Antarctica/McMurdo:20060922T155149 +-- X-MOZ-LOCATIONPATH:9a495928-276c-406b-8acd-e0883dfe68e3.ics +-- LOCATION:At work... +-- DESCRIPTION:This needs to be done. +-- URL:http://mcmillan.net.nz/ +-- END:VTODO + +-- The parsed todo. Here we have pulled those todos apart somewhat. +CREATE TABLE todo ( + user_no INT references usr(user_no), + dav_name TEXT, + dav_etag TEXT, + + -- Extracted VTODO data + uid TEXT, + created TIMESTAMP, + last_modified TIMESTAMP, + dtstamp TIMESTAMP, + dtstart TIMESTAMP WITH TIME ZONE, + dtend TIMESTAMP WITH TIME ZONE, + due TIMESTAMP WITH TIME ZONE, + priority INT, + summary TEXT, + location TEXT, + description TEXT, + class TEXT, + transp TEXT, + rrule TEXT, + url TEXT, + percent_complete NUMERIC(7,2), + tz_id TEXT REFERENCES time_zone( tz_id ), + + -- Cascade updates / deletes from the caldav_data table + CONSTRAINT caldav_exists FOREIGN KEY ( user_no, dav_name ) + REFERENCES caldav_data ( user_no, dav_name ) + MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE +); + +GRANT SELECT,INSERT,UPDATE,DELETE ON todo TO general; + -- Each user can be related to each other user. This mechanism can also -- be used to define groups of users, since some relationships are transitive. CREATE TABLE relationship_type ( diff --git a/inc/caldav-DELETE.php b/inc/caldav-DELETE.php index f8ad690d..a4a36eaa 100644 --- a/inc/caldav-DELETE.php +++ b/inc/caldav-DELETE.php @@ -7,9 +7,17 @@ dbg_error_log("delete", "DELETE method handler"); $get_path = $_SERVER['PATH_INFO']; $etag_none_match = str_replace('"','',$_SERVER["HTTP_IF_NONE_MATCH"]); -$qry = new PgQuery( "SELECT * FROM vevent_data WHERE user_no = ? AND vevent_name = ? AND vevent_etag = ?;", $session->user_no, $get_path, $etag_none_match ); +if ( $etag_none_match != '' ) { + /** + * etag_none_match is saying that we should only delete a row if it matches this etag + * (only rows not matching should exist afterwards, I guess.) + */ + $only_this_etag = " AND dav_etag = ".qpg($etag_none_match); +} + +$qry = new PgQuery( "SELECT * FROM caldav_data WHERE user_no = ? AND dav_name = ? $only_this_etag;", $session->user_no, $get_path ); if ( $qry->Exec("caldav-DELETE") && $qry->rows == 1 ) { - $qry = new PgQuery( "DELETE FROM vevent_data WHERE user_no = ? AND vevent_name = ? AND vevent_etag = ?;", $session->user_no, $get_path, $etag_none_match ); + $qry = new PgQuery( "DELETE FROM caldav_data WHERE user_no = ? AND dav_name = ? $only_this_etag;", $session->user_no, $get_path ); if ( $qry->Exec("caldav-DELETE") ) { header("HTTP/1.1 200 OK"); dbg_error_log( "delete", "DELETE: User: %d, ETag: %s, Path: %s", $session->user_no, $etag_none_match, $get_path); diff --git a/inc/caldav-GET.php b/inc/caldav-GET.php index 30c742a0..8672d090 100644 --- a/inc/caldav-GET.php +++ b/inc/caldav-GET.php @@ -7,23 +7,23 @@ dbg_error_log("get", "GET method handler"); $get_path = $_SERVER['PATH_INFO']; $etag_none_match = str_replace('"','',$_SERVER["HTTP_IF_NONE_MATCH"]); -$qry = new PgQuery( "SELECT * FROM vevent_data WHERE user_no = ? AND vevent_name = ? ;", $session->user_no, $get_path); +$qry = new PgQuery( "SELECT * FROM caldav_data WHERE user_no = ? AND dav_name = ? ;", $session->user_no, $get_path); dbg_error_log("get", "%s", $qry->querystring ); if ( $qry->Exec("GET") && $qry->rows == 1 ) { $event = $qry->Fetch(); header("HTTP/1.1 200 OK"); - header("ETag: $event->vevent_etag"); + header("ETag: $event->dav_etag"); header("Content-Type: text/calendar"); - print $event->vevent_data; + print $event->caldav_data; - dbg_error_log( "GET", "User: %d, ETag: %s, Path: %s", $session->user_no, $event->vevent_etag, $get_path); + dbg_error_log( "GET", "User: %d, ETag: %s, Path: %s", $session->user_no, $event->dav_etag, $get_path); } else if ( $qry->rows != 1 ) { header("HTTP/1.1 500 Internal Server Error"); - dbg_error_log("ERROR", "Multiple rows match for User: %d, ETag: %s, Path: %s", $session->user_no, $event->vevent_etag, $get_path); + dbg_error_log("ERROR", "Multiple rows match for User: %d, ETag: %s, Path: %s", $session->user_no, $event->dav_etag, $get_path); } else { header("HTTP/1.1 500 Infernal Server Error"); diff --git a/inc/caldav-OPTIONS.php b/inc/caldav-OPTIONS.php index adf3fcf5..fdad0a39 100644 --- a/inc/caldav-OPTIONS.php +++ b/inc/caldav-OPTIONS.php @@ -1,6 +1,8 @@ \ No newline at end of file diff --git a/inc/caldav-PUT.php b/inc/caldav-PUT.php index 254bce49..0c8a6b8d 100644 --- a/inc/caldav-PUT.php +++ b/inc/caldav-PUT.php @@ -28,7 +28,7 @@ if ( $etag_match == '*' || $etag_match == '' ) { * If they didn't send an etag_match header, we need to check if the PUT object already exists * and we are hence updating it. And we just set our etag_match to that. */ - $qry = new PgQuery( "SELECT * FROM vevent_data WHERE user_no=? AND vevent_name=?", $session->user_no, $put_path ); + $qry = new PgQuery( "SELECT * FROM caldav_data WHERE user_no=? AND dav_name=?", $session->user_no, $put_path ); $qry->Exec("PUT"); if ( $qry->rows > 1 ) { header("HTTP/1.1 500 Infernal Server Error"); @@ -37,7 +37,7 @@ if ( $etag_match == '*' || $etag_match == '' ) { } elseif ( $qry->rows == 1 ) { $event = $qry->Fetch(); - $etag_match = $event->vevent_etag; + $etag_match = $event->dav_etag; } } @@ -45,45 +45,54 @@ if ( $etag_match == '*' || $etag_match == '' ) { /** * If we got this far without an etag we must be inserting it. */ - $qry = new PgQuery( "INSERT INTO vevent_data ( user_no, vevent_name, vevent_etag, vevent_data, logged_user ) VALUES( ?, ?, ?, ?, ?)", - $session->user_no, $put_path, $etag, $raw_post, $session->user_no ); + $qry = new PgQuery( "INSERT INTO caldav_data ( user_no, dav_name, dav_etag, caldav_data, caldav_type, logged_user ) VALUES( ?, ?, ?, ?, ?, ?)", + $session->user_no, $put_path, $etag, $raw_post, $ev->type, $session->user_no ); $qry->Exec("PUT"); header("HTTP/1.1 201 Created"); header("ETag: $etag"); - dbg_error_log( "PUT", "INSERT INTO vevent_data ( user_no, vevent_name, vevent_etag, vevent_data, logged_user ) VALUES( %d, '%s', '%s', '%s', %d)", - $session->user_no, $put_path, $etag, $raw_post, $session->user_no ); } else { - $qry = new PgQuery( "UPDATE vevent_data SET vevent_data=?, vevent_etag=?, logged_user=? WHERE user_no=? AND vevent_name=? AND vevent_etag=?", - $raw_post, $etag, $session->user_no, $session->user_no, $put_path, $etag_match ); + $qry = new PgQuery( "UPDATE caldav_data SET caldav_data=?, dav_etag=?, caldav_type=?, logged_user=? WHERE user_no=? AND dav_name=? AND dav_etag=?", + $raw_post, $etag, $ev->type, $session->user_no, $session->user_no, $put_path, $etag_match ); $qry->Exec("PUT"); header("HTTP/1.1 201 Replaced"); header("ETag: $etag"); } -$sql = "SET TIMEZONE TO ".qpg($ev->tz_locn).";"; +if ( $ev->type == 'VEVENT' ) $table = 'event'; +elseif ( $ev->type == 'VTODO' ) $table = 'todo'; + +$sql = ( $ev->tz_locn == '' ? '' : "SET TIMEZONE TO ".qpg($ev->tz_locn).";" ); + if ( $etag_match == '*' || $etag_match == '' ) { $sql .= <<user_no, $put_path, $etag, $ev->Get('uid'), $ev->Get('dtstamp'), $ev->Get('dtstart'), $ev->Get('dtend'), $ev->Get('summary'), $ev->Get('location'), - $ev->Get('class'), $ev->Get('transp'), $ev->Get('description'), $ev->Get('rrule'), $ev->Get('tz_id') ); + $ev->Get('class'), $ev->Get('transp'), $ev->Get('description'), $ev->Get('rrule'), $ev->Get('tz_id'), + $ev->Get('last-modified'), $ev->Get('url'), $ev->Get('priority'), $ev->Get('created'), + $ev->Get('due'), $ev->Get('percent-complete') + ); $qry->Exec("PUT"); } else { $sql = <<Get('uid'), $ev->Get('dtstamp'), $ev->Get('dtstart'), $ev->Get('dtend'), $ev->Get('summary'), $ev->Get('location'), $ev->Get('class'), $ev->Get('transp'), $ev->Get('description'), $ev->Get('rrule'), - $ev->Get('tz_id'), $session->user_no, $put_path, $etag ); + $ev->Get('tz_id'), $ev->Get('last-modified'), $ev->Get('url'), $ev->Get('priority'), $etag, + $ev->Get('due'), $ev->Get('percent-complete'), + $session->user_no, $put_path ); $qry->Exec("PUT"); } diff --git a/inc/caldav-REPORT.php b/inc/caldav-REPORT.php index 07b3ae53..514bacad 100644 --- a/inc/caldav-REPORT.php +++ b/inc/caldav-REPORT.php @@ -28,6 +28,18 @@ foreach( $rpt_request AS $k => $v ) { switch ( $v['tag'] ) { + case 'URN:IETF:PARAMS:XML:NS:CALDAV:CALENDAR-MULTIGET': + dbg_log_array( "REPORT", "CALENDAR-MULTIGET", $v, true ); + $report[$reportnum]['multiget'] = 1; + if ( $v['type'] == "open" ) { + $multiget_names = array(); + } + else if ( $v['type'] == "close" ) { + $report[$reportnum]['get_names'] = $multiget_names; + unset($multiget_names); + } + break; + case 'URN:IETF:PARAMS:XML:NS:CALDAV:CALENDAR-DATA': dbg_log_array( "REPORT", "CALENDAR-DATA", $v, true ); if ( $v['type'] == "complete" ) { @@ -109,6 +121,12 @@ foreach( $rpt_request AS $k => $v ) { } break; + case 'DAV::HREF': + dbg_log_array( "REPORT", "DAV::HREF", $v, true ); + if ( isset($report[$reportnum]['multiget']) ) { + $multiget_names[] = $v['value']; + } + default: dbg_error_log( "REPORT", "Unhandled tag >>".$v['tag']."<<"); } @@ -172,29 +190,54 @@ REPORTHDR; if ( isset($report[$i]['calendar-event']) ) { if ( isset($report[$i]['include_href']) ) dbg_error_log( "REPORT", "Returning href event data" ); if ( isset($report[$i]['include_data']) ) dbg_error_log( "REPORT", "Returning full event data" ); - $sql = "SELECT * FROM vevent_data NATURAL JOIN event "; + $sql = "SELECT * FROM caldav_data NATURAL JOIN event WHERE caldav_type = 'VEVENT' "; $where = ""; if ( isset( $report[$i]['start'] ) ) { - $where = "WHERE dtend >= ".qpg($report[$i]['start'])."::timestamp with time zone "; + $where = "AND (dtend >= ".qpg($report[$i]['start'])."::timestamp with time zone "; + $where .= "OR calculate_later_timestamp(".qpg($report[$i]['start'])."::timestamp with time zone,dtend,rrule) >= ".qpg($report[$i]['start'])."::timestamp with time zone) "; } if ( isset( $report[$i]['end'] ) ) { - if ( $where != "" ) $where .= "AND "; - $where .= "dtstart <= ".qpg($report[$i]['end'])."::timestamp with time zone "; + $where .= "AND dtstart <= ".qpg($report[$i]['end'])."::timestamp with time zone "; } $sql .= $where; $qry = new PgQuery( $sql ); if ( $qry->Exec() && $qry->rows > 0 ) { while( $event = $qry->Fetch() ) { - $calhref = ( isset($report[$i]['include_href']) ? sprintf( $calendar_href_tpl, $_SERVER['SERVER_NAME'], $_SERVER['SERVER_PORT'], $_SERVER['SCRIPT_NAME'], $event->vevent_name ) : "" ); - $caldata = ( isset($report[$i]['include_data']) ? sprintf( $calendar_data_tpl, $event->vevent_data ) : "" ); - printf( $response_tpl, $calhref, $event->vevent_etag, $caldata ); - dbg_error_log("REPORT", "ETag >>%s<< >>http://%s:%s%s%s<<", $event->vevent_etag, - $_SERVER['SERVER_NAME'], $_SERVER['SERVER_PORT'], $_SERVER['SCRIPT_NAME'], $event->vevent_name); + $calhref = ( isset($report[$i]['include_href']) ? sprintf( $calendar_href_tpl, $_SERVER['SERVER_NAME'], $_SERVER['SERVER_PORT'], $_SERVER['SCRIPT_NAME'], $event->dav_name ) : "" ); + $caldata = ( isset($report[$i]['include_data']) ? sprintf( $calendar_data_tpl, $event->caldav_data ) : "" ); + printf( $response_tpl, $calhref, $event->dav_etag, $caldata ); + dbg_error_log("REPORT", "ETag >>%s<< >>http://%s:%s%s%s<<", $event->dav_etag, + $_SERVER['SERVER_NAME'], $_SERVER['SERVER_PORT'], $_SERVER['SCRIPT_NAME'], $event->dav_name); } } } + if ( isset($report[$i]['calendar-todo']) ) { - if ( isset($report[$i]['include_data']) ) dbg_error_log( "REPORT", "FIXME: Not returning full todo data" ); + /** + * Produce VTODO data. + */ + if ( isset($report[$i]['include_href']) ) dbg_error_log( "REPORT", "Returning href event data" ); + if ( isset($report[$i]['include_data']) ) dbg_error_log( "REPORT", "Returning full event data" ); + $sql = "SELECT * FROM caldav_data NATURAL JOIN todo WHERE caldav_type = 'VTODO' "; + $where = ""; + if ( isset( $report[$i]['start'] ) ) { + $where = "AND (dtend >= ".qpg($report[$i]['start'])."::timestamp with time zone "; + $where .= "OR calculate_later_timestamp(".qpg($report[$i]['start'])."::timestamp with time zone,dtend,rrule) >= ".qpg($report[$i]['start'])."::timestamp with time zone) "; + } + if ( isset( $report[$i]['end'] ) ) { + $where .= "AND dtstart <= ".qpg($report[$i]['end'])."::timestamp with time zone "; + } + $sql .= $where; + $qry = new PgQuery( $sql ); + if ( $qry->Exec() && $qry->rows > 0 ) { + while( $event = $qry->Fetch() ) { + $calhref = ( isset($report[$i]['include_href']) ? sprintf( $calendar_href_tpl, $_SERVER['SERVER_NAME'], $_SERVER['SERVER_PORT'], $_SERVER['SCRIPT_NAME'], $event->dav_name ) : "" ); + $caldata = ( isset($report[$i]['include_data']) ? sprintf( $calendar_data_tpl, $event->caldav_data ) : "" ); + printf( $response_tpl, $calhref, $event->dav_etag, $caldata ); + dbg_error_log("REPORT", "ETag >>%s<< >>http://%s:%s%s%s<<", $event->dav_etag, + $_SERVER['SERVER_NAME'], $_SERVER['SERVER_PORT'], $_SERVER['SCRIPT_NAME'], $event->dav_name); + } + } } if ( isset($report[$i]['calendar-freebusy']) ) { if ( isset($report[$i]['include_data']) ) dbg_error_log( "REPORT", "FIXME: Not returning full freebusy data" ); diff --git a/inc/vEvent.php b/inc/vEvent.php index 1d949e33..7b557987 100644 --- a/inc/vEvent.php +++ b/inc/vEvent.php @@ -38,6 +38,12 @@ class vEvent { */ var $tz_locn; + /** + * The type of iCalendar data VEVENT/VTODO + * @var type string + */ + var $type; + /**#@-*/ /** @@ -49,13 +55,10 @@ class vEvent { global $c; // Probably a good idea to always have values for these things... - $this->properties['tz_id'] = $c->local_tzid; - $this->properties['modified'] = time(); + if ( isset($c->local_tzid ) ) $this->properties['tz_id'] = $c->local_tzid; + $this->properties['dtstamp'] = date('Ymd\THis'); $this->properties['sequence'] = 1; $this->properties['uid'] = sprintf( "%s@%s", time() * 1000 + rand(0,1000), $c->domain_name); - $this->properties['guid'] = sprintf( "%s@%s", time() * 1000 + rand(0,1000), $c->domain_name); - $this->properties['duration'] = "PT1H"; - $this->properties['status'] = "TENTATIVE"; if ( !isset($args) || !is_array($args) ) return; @@ -87,7 +90,14 @@ class vEvent { switch( $state ) { case 0: - if ( $v == 'BEGIN:VEVENT' ) $state = $v; + if ( $v == 'BEGIN:VEVENT' ) { + $state = $v; + $this->type = 'VEVENT'; + } + else if ( $v == 'BEGIN:VTODO' ) { + $state = $v; + $this->type = 'VTODO'; + } else if ( $v == 'BEGIN:VTIMEZONE' ) $state = $v; break; @@ -95,6 +105,10 @@ class vEvent { if ( $v == 'END:VEVENT' ) $state = 0; break; + case 'BEGIN:VTODO': + if ( $v == 'END:VTODO' ) $state = 0; + break; + case 'BEGIN:VTIMEZONE': if ( $v == 'END:VTIMEZONE' ) { $state = 0; @@ -103,7 +117,7 @@ class vEvent { break; } - if ( $state == 'BEGIN:VEVENT' && $state != $v ) { + if ( ($state == 'BEGIN:VEVENT' || $state == 'BEGIN:VTODO') && $state != $v ) { list( $parameter, $value ) = preg_split('/:/', $v ); if ( preg_match('/^DT[A-Z]+;TZID=/', $parameter) ) { list( $parameter, $tz_id ) = preg_split('/;/', $parameter ); @@ -133,22 +147,27 @@ class vEvent { * them into something that PostgreSQL can understand... */ function DealWithTimeZones() { - $qry = new PgQuery( "SELECT tz_locn FROM time_zone WHERE tz_id = ?;", $this->properties['TZID'] ); - if ( $qry->Exec('vEvent') && $qry->rows == 1 ) { - $row = $qry->Fetch(); - $this->tz_locn = $row->tz_locn; - } - else { - if ( !isset($this->tz_locn) ) { - // In case there was no X-LIC-LOCATION defined, let's hope there is something in the TZID - $this->tz_locn = preg_replace('/^.*([a-z]+\/[a-z]+)$/i','$1',$this->properties['TZID'] ); + if ( isset($c->save_time_zone_defs) ) { + $qry = new PgQuery( "SELECT tz_locn FROM time_zone WHERE tz_id = ?;", $this->properties['TZID'] ); + if ( $qry->Exec('vEvent') && $qry->rows == 1 ) { + $row = $qry->Fetch(); + $this->tz_locn = $row->tz_locn; } + } + + if ( !isset($this->tz_locn) ) { + // In case there was no X-LIC-LOCATION defined, let's hope there is something in the TZID + $this->tz_locn = preg_replace('/^.*([a-z]+\/[a-z]+)$/i','$1',$this->properties['TZID'] ); + } + + if ( isset($c->save_time_zone_defs) && $qry->rows != 1 ) { $qry2 = new PgQuery( "INSERT INTO time_zone (tz_id, tz_locn, tz_spec) VALUES( ?, ?, ? );", $this->properties['TZID'], $this->tz_locn, $this->properties['VTIMEZONE'] ); $qry2->Exec("vEvent"); } } + /** * Get the value of a property */ @@ -156,6 +175,114 @@ class vEvent { return $this->properties[strtoupper($key)]; } + + /** + * Put the value of a property + */ + function Put( $key, $value ) { + return $this->properties[strtoupper($key)] = $value; + } + + + /** + * Returns a PostgreSQL Date Format string suitable for returning iCal dates + */ + function SqlDateFormat() { + return "'IYYYMMDD\"T\"HH24MISS'"; + } + + + /** + * Returns a PostgreSQL Date Format string suitable for returning iCal durations + * - this doesn't work for negative intervals, but events should not have such! + */ + function SqlDurationFormat() { + return "'\"PT\"HH24\"H\"MI\"M\"'"; + } + + +/* +BEGIN:VCALENDAR +CALSCALE:GREGORIAN +PRODID:-//Ximian//NONSGML Evolution Calendar//EN +VERSION:2.0 +BEGIN:VEVENT +UID:20060918T005755Z-21151-1000-1-7@ubu +DTSTAMP:20060918T005755Z +DTSTART;TZID=/softwarestudio.org/Olson_20011030_5/Pacific/Auckland: + 20060918T153000 +DTEND;TZID=/softwarestudio.org/Olson_20011030_5/Pacific/Auckland: + 20060918T160000 +SUMMARY:Lunch +X-EVOLUTION-CALDAV-HREF:http: + //andrew@mycaldav/caldav.php/andrew/20060918T005757Z.ics +BEGIN:VALARM +X-EVOLUTION-ALARM-UID:20060918T005755Z-21149-1000-1-12@ubu +ACTION:DISPLAY +TRIGGER;VALUE=DURATION;RELATED=START:-PT15M +DESCRIPTION:Lunch +END:VALARM +END:VEVENT +BEGIN:VTIMEZONE +TZID:/softwarestudio.org/Olson_20011030_5/Pacific/Auckland +X-LIC-LOCATION:Pacific/Auckland +BEGIN:STANDARD +TZOFFSETFROM:+1300 +TZOFFSETTO:+1200 +TZNAME:NZST +DTSTART:19700315T030000 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=3SU;BYMONTH=3 +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETFROM:+1200 +TZOFFSETTO:+1300 +TZNAME:NZDT +DTSTART:19701004T020000 +RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=1SU;BYMONTH=10 +END:DAYLIGHT +END:VTIMEZONE +END:VCALENDAR +*/ + /** + * Render the vEvent object as a text string which is a single VEVENT + */ + function Render( ) { + $interesting = array( "uid", "dtstamp", "dtstart", "dtend", "duration", "summary", + "location", "description", "action", "class", "transp", "sequence"); + + $result = << $v ) { + $v = strtoupper($v); + if ( isset($this->properties[$v]) ) + $result .= sprintf("%s:%s\n", $v, $this->properties[$v]); + } + $result .= <<properties['UID'], + $this->properties['DTSTART'], + $this->properties['DURATION'], + $this->properties['SUMMARY'], + $this->properties['LOCATION'] + ); +*/ + return $result; + } + + } -?> \ No newline at end of file +?> diff --git a/rscds.webprj b/rscds.webprj index 0279d1a0..4e4384cf 100644 --- a/rscds.webprj +++ b/rscds.webprj @@ -41,5 +41,6 @@ +