diff --git a/config/config.php b/config/config.php
index 3703fbdd..fae3d1fa 100644
--- a/config/config.php
+++ b/config/config.php
@@ -3,11 +3,13 @@
// $c->sysabbr = 'rscds';
// $c->admin_email = 'andrew@catalyst.net.nz';
// $c->system_name = "Really Simple CalDAV Store";
+// $c->collections_always_exist = false;
$c->pg_connect[] = 'dbname=caldav port=5433 user=general';
$c->pg_connect[] = 'dbname=caldav port=5432 user=general';
$c->dbg['ALL'] = 1;
- $debuggroups['querystring'] = 1;
+
+ $c->collections_always_exist = false;
?>
\ No newline at end of file
diff --git a/dba/rscds.sql b/dba/rscds.sql
index bf784d72..e0677751 100644
--- a/dba/rscds.sql
+++ b/dba/rscds.sql
@@ -10,6 +10,8 @@ CREATE TABLE caldav_data (
user_no INT references usr(user_no),
dav_name TEXT,
dav_etag TEXT,
+ created TIMESTAMP WITH TIME ZONE,
+ modified TIMESTAMP WITH TIME ZONE,
caldav_data TEXT,
caldav_type TEXT,
logged_user INT references usr(user_no),
@@ -28,13 +30,13 @@ CREATE TABLE time_zone (
);
GRANT SELECT,INSERT ON time_zone TO general;
--- The parsed event. Here we have pulled those events apart somewhat.
-CREATE TABLE event (
+-- The parsed calendar item. Here we have pulled those events/todos/journals apart somewhat.
+CREATE TABLE calendar_item (
user_no INT references usr(user_no),
dav_name TEXT,
dav_etag TEXT,
- -- Extracted vEvent event data
+ -- Extracted vEvent/vTodo data
uid TEXT,
created TIMESTAMP,
last_modified TIMESTAMP,
@@ -59,67 +61,24 @@ CREATE TABLE event (
MATCH FULL ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE
);
-GRANT SELECT,INSERT,UPDATE,DELETE ON event TO general;
+GRANT SELECT,INSERT,UPDATE,DELETE ON calendar_item 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;
-- Something that can look like a filesystem hierarchy where we store stuff
-CREATE TABLE calendar (
+CREATE TABLE collection (
user_no INT references usr(user_no),
+ parent_container TEXT,
dav_name TEXT,
dav_etag TEXT,
+ dav_displayname TEXT,
+ is_calendar BOOLEAN,
created TIMESTAMP WITH TIME ZONE,
+ modified TIMESTAMP WITH TIME ZONE,
PRIMARY KEY ( user_no, dav_name )
);
-GRANT SELECT,INSERT,UPDATE,DELETE ON calendar TO general;
+GRANT SELECT,INSERT,UPDATE,DELETE ON collection 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.
diff --git a/inc/XMLElement.php b/inc/XMLElement.php
deleted file mode 100644
index efdea837..00000000
--- a/inc/XMLElement.php
+++ /dev/null
@@ -1,125 +0,0 @@
-
-* @copyright Catalyst .Net Ltd
-* @license http://gnu.org/copyleft/gpl.html GNU GPL v2
-*/
-require_once("AWLUtilities.php");
-
-/**
-* A class for XML elements which may have attributes, or contain
-* other XML sub-elements
-*
-* @package awl
-*/
-class XMLElement {
- var $tagname;
- var $attributes;
- var $content;
-
- /**
- * Constructor - nothing fancy as yet.
- *
- * @param string The tag name of the new element
- * @param mixed Either a string of content, or an array of sub-elements
- * @param array An array of attribute name/value pairs
- */
- function XMLElement( $tagname, $content=false, $attributes=false ) {
- $this->tagname=$tagname;
- $this->content=$content;
- $this->attributes = $attributes;
- }
-
- /**
- * Set an element attribute to a value
- *
- * @param string The attribute name
- * @param string The attribute value
- */
- function SetAttribute($k,$v) {
- if ( gettype($this->attributes) != "array" ) $this->attributes = array();
- $this->attributes[$k] = $v;
- }
-
- /**
- * Set the whole content to a value
- *
- * @param mixed The element content, which may be text, or an array of sub-elements
- */
- function SetContent($v) {
- $this->content = $v;
- }
-
- /**
- * Add a sub-element
- *
- * @param object An XMLElement to be appended to the array of sub-elements
- */
- function AddSubTag($v) {
- if ( gettype($this->content) != "array" ) $this->content = array();
- $this->content[] = $v;
- }
-
- /**
- * Add a new sub-element
- *
- * @param string The tag name of the new element
- * @param mixed Either a string of content, or an array of sub-elements
- * @param array An array of attribute name/value pairs
- */
- function NewElement( $tagname, $content=false, $attributes=false ) {
- if ( gettype($this->content) != "array" ) $this->content = array();
- $this->content[] = new XMLElement($tagname,$content,$attributes);
- }
-
- /**
- * Render the document tree into (nicely formatted) XML
- *
- * @param int The indenting level for the pretty formatting of the element
- */
- function Render($indent=0) {
- $r = substr(" ",0,$indent) . '<' . $this->tagname;
- if ( gettype($this->attributes) == "array" ) {
- /**
- * Render the element attribute values
- */
- foreach( $this->attributes AS $k => $v ) {
- $r .= sprintf( ' %s="%s"', $k, htmlspecialchars($v) );
- }
- }
- if ( (is_array($this->content) && count($this->content) > 0) || strlen($this->content) > 0 ) {
- $r .= ">";
- if ( is_array($this->content) ) {
- /**
- * Render the sub-elements with a deeper indent level
- */
- $r .= "\n";
- foreach( $this->content AS $k => $v ) {
- if ( is_object($v) ) {
- $r .= $v->Render($indent+1);
- }
- }
- $r .= substr(" ",0,$indent);
- }
- else {
- /**
- * Render the content, with special characters escaped
- *
- * FIXME This should switch to CDATA in some situations.
- */
- $r .= htmlspecialchars($this->content, ENT_NOQUOTES );
- }
- $r .= '' . $this->tagname.">\n";
- }
- else {
- $r .= "/>\n";
- }
- return $r;
- }
-}
-
-?>
\ No newline at end of file
diff --git a/inc/caldav-MKCALENDAR.php b/inc/caldav-MKCALENDAR.php
index 2b666f82..7382aa84 100644
--- a/inc/caldav-MKCALENDAR.php
+++ b/inc/caldav-MKCALENDAR.php
@@ -2,14 +2,70 @@
dbg_error_log("MKCALENDAR", "method handler");
+dbg_log_array( "MKCOL", 'HEADERS', $raw_headers );
+dbg_log_array( "MKCOL", '_SERVER', $_SERVER, true );
+dbg_error_log( "MKCOL", "RAW: %s", str_replace("\n", "",str_replace("\r", "", $raw_post)) );
+
$make_path = $_SERVER['PATH_INFO'];
-$sql = "INSERT INTO calendar ( user_no, dav_name, dav_etag, created ) VALUES( ?, ?, ?, current_timestamp );";
-$qry = new PgQuery( $sql, $session->user_no, $make_path, md5($session->user_no. $make_path) );
+$displayname = $make_path;
+$parent_container = '/';
+if ( preg_match( '#^(.*/)([^/]+)(/)?$#', $make_path, $matches ) ) {
+ $parent_container = $matches[1];
+ $displayname = $matches[2];
+}
-if ( $qry->Exec("MKCALENDAR",__LINE__,__FILE__) )
+$sql = "INSERT INTO collection ( user_no, parent_container, dav_name, dav_etag, dav_displayname, is_calendar, created, modified ) VALUES( ?, ?, ?, ?, ?, TRUE, current_timestamp, current_timestamp );";
+$qry = new PgQuery( $sql, $session->user_no, $parent_container, $make_path, md5($session->user_no. $make_path), $displayname );
+
+if ( $qry->Exec("MKCALENDAR",__LINE__,__FILE__) ) {
header("HTTP/1.1 200 Created");
-else
+ dbg_error_log( "MKCALENDAR", "New calendar '%s' created named '%s' for user '%d' in parent '%s'", $make_path, $displayname, $session->user_no, $parent_container);
+}
+else {
header("HTTP/1.1 500 Infernal Server Error");
+ dbg_error_log( "ERROR", " MKCOL Failed for '%s' named '%s', user '%d' in parent '%s'", $make_path, $displayname, $session->user_no, $parent_container);
+}
+
+/**
+* We could also respond to the request...
+*
+
+
+
+
+ Lisa's Events
+ Calendar restricted to events.
+
+
+
+
+
+
+
+
+*/
?>
\ No newline at end of file
diff --git a/inc/caldav-PROPFIND.php b/inc/caldav-PROPFIND.php
index 23b20882..d98be87b 100644
--- a/inc/caldav-PROPFIND.php
+++ b/inc/caldav-PROPFIND.php
@@ -7,13 +7,13 @@ $parser = xml_parser_create_ns('UTF-8');
xml_parser_set_option ( $parser, XML_OPTION_SKIP_WHITE, 1 );
function xml_start_callback( $parser, $el_name, $el_attrs ) {
- dbg_error_log( "PROPFIND", "Parsing $el_name" );
- dbg_log_array( "PROPFIND", "$el_name::attrs", $el_attrs, true );
+// dbg_error_log( "PROPFIND", "Parsing $el_name" );
+// dbg_log_array( "PROPFIND", "$el_name::attrs", $el_attrs, true );
$attributes[$el_name] = $el_attrs;
}
function xml_end_callback( $parser, $el_name ) {
- dbg_error_log( "PROPFIND", "Finished Parsing $el_name" );
+// dbg_error_log( "PROPFIND", "Finished Parsing $el_name" );
}
xml_set_element_handler ( $parser, 'xml_start_callback', 'xml_end_callback' );
@@ -27,139 +27,322 @@ list( $blank, $username, $calpath ) = split( '/', $find_path, 3);
$calpath = "/".$calpath;
$href_list = array();
$attribute_list = array();
+$depth = $_SERVER['HTTP_DEPTH'];
+if ( $depth == 'infinite' ) $depth = 99;
+else $depth = intval($depth);
+
+// dbg_log_array("PROPFIND","_SERVER", $_SERVER, true );
if ( isset($debugging) ) {
$attribute_list = array( 'GETETAG' => 1, 'GETCONTENTLENGTH' => 1, 'GETCONTENTTYPE' => 1, 'RESOURCETYPE' => 1 );
+ $depth = 1;
}
+$unsupported = array();
foreach( $rpt_request AS $k => $v ) {
- switch ( $v['tag'] ) {
-
+ $tag = $v['tag'];
+ switch ( $tag ) {
case 'DAV::PROPFIND':
- dbg_log_array( "PROPFIND", "DAV-PROPFIND", $v, true );
+ dbg_error_log( "PROPFIND", ":Request: %s -> %s", $v['type'], $tag );
+// dbg_log_array( "PROPFIND", "DAV-PROPFIND", $v, true );
break;
case 'DAV::PROP':
- dbg_log_array( "PROPFIND", "DAV::PROP", $v, true );
+ dbg_error_log( "PROPFIND", ":Request: %s -> %s", $v['type'], $tag );
+// dbg_log_array( "PROPFIND", "DAV::PROP", $v, true );
break;
case 'DAV::GETETAG':
+ case 'DAV::DISPLAYNAME':
case 'DAV::GETCONTENTLENGTH':
case 'DAV::GETCONTENTTYPE':
case 'DAV::RESOURCETYPE':
+ case 'DAV::CURRENT-USER-PRIVILEGE-SET':
$attribute = substr($v['tag'],5);
$attribute_list[$attribute] = 1;
+ dbg_error_log( "PROPFIND", "Adding attribute '%s'", $attribute );
break;
case 'DAV::HREF':
- dbg_log_array( "PROPFIND", "DAV::HREF", $v, true );
+ // dbg_log_array( "PROPFIND", "DAV::HREF", $v, true );
$href_list[] = $v['value'];
- break;
default:
- dbg_error_log( "PROPFIND", "Unhandled tag >>".$v['tag']."<<");
+ if ( preg_match('/^(.*):([^:]+)$/', $tag, $matches) ) {
+ $unsupported[$matches[2]] = $matches[1];
+ }
+ else {
+ $unsupported[$tag] = "";
+ }
+ dbg_error_log( "PROPFIND", "Unhandled tag >>%s<<", $tag);
}
}
+require_once("XMLElement.php");
+
+/**
+* Returns the array of privilege names converted into XMLElements
+*/
+function privileges($privilege_names) {
+ $privileges = array();
+ foreach( $privilege_names AS $k => $v ) {
+ $privileges[] = new XMLElement("privilege", new XMLElement($v));
+ }
+ return $privileges;
+}
+
+/**
+* Returns an XML sub-tree for a single collection record from the DB
+*/
+function collection_to_xml( $collection ) {
+ global $attribute_list, $session, $c;
+
+ dbg_error_log("PROPFIND","Building XML Response for collection '%s'", $collection->dav_name );
+
+ $url = $_SERVER['SCRIPT_NAME'] . $collection->dav_name;
+ $resourcetypes = array( new XMLElement("collection") );
+ $contentlength = false;
+ if ( $collection->is_calendar == 't' ) {
+ $resourcetypes[] = new XMLElement("calendar", false, array("xmlns" => "urn:ietf:params:xml:ns:caldav"));
+ $lqry = new PgQuery("SELECT sum(length(caldav_data)) FROM caldav_data WHERE user_no = ? AND dav_name ~ ?;", $user_no, $collection_path.'[^/]+$' );
+ if ( $lqry->Exec("PROPFIND",__LINE,__FILE__) && $row = $lqry->Fetch() ) {
+ $contentlength = $row->sum;
+ }
+ }
+ $prop = new XMLElement("prop");
+ if ( isset($attribute_list['GETCONTENTLENGTH']) ) {
+ $prop->NewElement("getcontentlength", $contentlength );
+ }
+ if ( isset($attribute_list['GETCONTENTTYPE']) ) {
+ // $prop->NewElement("getcontenttype", "text/calendar" );
+ $prop->NewElement("getcontenttype", "httpd/unix-directory" );
+ }
+ if ( isset($attribute_list['RESOURCETYPE']) ) {
+ $prop->NewElement("resourcetype", $resourcetypes );
+ }
+ if ( isset($attribute_list['DISPLAYNAME']) ) {
+ $displayname = ( $collection->caldav_displayname == "" ? ucfirst(trim(str_replace("/"," ", $collection->dav_name))) : $collection->caldav_displayname );
+ $prop->NewElement("displayname", $displayname );
+ }
+ if ( isset($attribute_list['GETETAG']) ) {
+ $prop->NewElement("getetag", '"'.$collection->dav_etag.'"' );
+ }
+ if ( isset($attribute_list['CURRENT-USER-PRIVILEGE-SET']) ) {
+ /**
+ * FIXME: Fairly basic set of privileges at present.
+ */
+ if ( $session->AllowedTo("Admin") && preg_match("#/.+/#", $collection->dav_name) ) {
+ $privs = array("all");
+ }
+ else {
+ $privs = array("read");
+ if ( $session->user_no == $collection->user_no || $session->AllowedTo("Admin") ) {
+ $privs[] = "write";
+ }
+ }
+ $prop->NewElement("current-user-privilege-set", privileges($privs) );
+ }
+ $status = new XMLElement("status", "HTTP/1.1 200 OK" );
+
+ $propstat = new XMLElement( "propstat", array( $prop, $status) );
+ $href = new XMLElement("href", $url );
+
+ $response = new XMLElement( "response", array($href,$propstat));
+
+ return $response;
+}
+
+
/**
-* Here is the kind of thing we are going to do, returning a top-level collection
-* response, followed by a response for each calendar (or other resource) within it.
-*
-*
-*
-* /caldav.php/path/they/sent/
-*
-*
-*
-* httpd/unix-directory
-*
-*
-*
-*
-* HTTP/1.1 200 OK
-*
-*
-*
-* /caldav.php/path/they/sent/calendar
-*
-*
-*
-* httpd/unix-directory
-*
-*
-*
-*
-*
-* HTTP/1.1 200 OK
-*
-*
-*
+* Return XML for a single data item from the DB
*/
+function item_to_xml( $item ) {
+ global $attribute_list, $session, $c;
-require_once("XMLElement.php");
+ dbg_error_log("PROPFIND","Building XML Response for item '%s'", $item->dav_name );
-if ( count($href_list) > 0 ) {
- // Not supported at this point...
- dbg_error_log("ERROR", " PROPFIND: Support for PROPFIND on specific URLs is not implemented");
+ $url = $_SERVER['SCRIPT_NAME'] . $item->dav_name;
+ $prop = new XMLElement("prop");
+ if ( isset($attribute_list['GETCONTENTLENGTH']) ) {
+ $contentlength = strlen($item->caldav_data);
+ $prop->NewElement("getcontentlength", $contentlength );
+ }
+ if ( isset($attribute_list['GETCONTENTTYPE']) ) {
+ $prop->NewElement("getcontenttype", "text/calendar" );
+ }
+ if ( isset($attribute_list['RESOURCETYPE']) ) {
+ $prop->NewElement("resourcetype", new XMLElement("calendar", false, array("xmlns" => "urn:ietf:params:xml:ns:caldav")) );
+ }
+ if ( isset($attribute_list['DISPLAYNAME']) ) {
+ $prop->NewElement("displayname");
+ }
+ if ( isset($attribute_list['GETETAG']) ) {
+ $prop->NewElement("getetag", '"'.$item->dav_etag.'"' );
+ }
+ if ( isset($attribute_list['CURRENT-USER-PRIVILEGE-SET']) ) {
+ /**
+ * FIXME: Fairly basic set of privileges at present.
+ */
+ if ( $session->AllowedTo("Admin") && preg_match("#/.+/.#", $item->dav_name) ) {
+ $privs = array("all");
+ }
+ else {
+ $privs = array("read");
+ if ( $session->user_no == $item->user_no || $session->AllowedTo("Admin") ) {
+ $privs[] = "write";
+ }
+ }
+ $prop->NewElement("current-user-privilege-set", privileges($privs) );
+ }
+ $status = new XMLElement("status", "HTTP/1.1 200 OK" );
+
+ $propstat = new XMLElement( "propstat", array( $prop, $status) );
+ $href = new XMLElement("href", $url );
+
+ $response = new XMLElement( "response", array($href,$propstat));
+
+ return $response;
+}
+
+/**
+* Get XML response for items in the collection
+* If '/' is requested, a list of (FIXME: visible) users is given, otherwise
+* a list of calendars for the user which are parented by this path.
+*
+* Permissions here might well be handled through an SQL function.
+*/
+function get_collection_contents( $depth, $user_no, $collection_path ) {
+ global $session;
+
+ dbg_error_log("PROPFIND","Getting collection contents: Depth %d, User: %d, Path: %s, IsCalendar: %s", $depth, $user_no, $collection_path, $collection->is_calendar );
+
+ $responses = array();
+ if ( $collection->is_calendar != 't' ) {
+ /**
+ * Calendar collections may not contain calendar collections.
+ */
+ if ( $collection_path == '/' ) {
+ $sql .= "SELECT user_no, '/' || username || '/' AS dav_name, md5( '/' || username || '/') AS dav_etag, ";
+ $sql .= "updated AS created, updated AS modified, fullname AS dav_displayname, FALSE AS is_calendar FROM usr";
+ }
+ else {
+ $sql = "SELECT dav_name, dav_etag, created, modified, dav_displayname, is_calendar FROM collection WHERE parent_container=".qpg($collection_path);
+ }
+ $qry = new PgQuery($sql);
+
+ if( $qry->Exec("PROPFIND",__LINE,__FILE__) && $qry->rows > 0 ) {
+ while( $collection = $qry->Fetch() ) {
+ $responses[] = collection_to_xml( $collection );
+ if ( $depth > 0 ) {
+ $responses = array_merge( $responses, get_collection( $depth - 1, $user_no, $collection->dav_name ) );
+ }
+ }
+ }
+ }
+
+ dbg_error_log("PROPFIND","Getting collection items: Depth %d, User: %d, Path: %s", $depth, $user_no, $collection_path );
+
+ $sql = "SELECT dav_name, caldav_data, dav_etag, created, modified FROM caldav_data WHERE dav_name ~ ".qpg('^'.$collection_path.'[^/]+$');
+ $qry = new PgQuery($sql);
+ if( $qry->Exec("PROPFIND",__LINE,__FILE__) && $qry->rows > 0 ) {
+ while( $item = $qry->Fetch() ) {
+ $responses[] = item_to_xml( $item );
+ }
+ }
+
+ return $responses;
+}
+
+/**
+* Get XML response for a single collection. If Depth is >0 then
+* subsidiary collections will also be got up to $depth
+*/
+function get_collection( $depth, $user_no, $collection_path ) {
+ global $c;
+ $responses = array();
+
+ dbg_error_log("PROPFIND","Getting collection: Depth %d, User: %d, Path: %s", $depth, $user_no, $collection_path );
+
+ if ( $collection_path == '/' ) {
+ $collection->dav_name = $collection_path;
+ $collection->dav_etag = md5($c->system_name . $collection_path);
+ $collection->is_calendar = 'f';
+ $collection->dav_displayname = $c->system_name;
+ $collection->created = date('Ymd"T"His');
+ $responses[] = collection_to_xml( $collection );
+ }
+ else {
+ $user_no = intval($user_no);
+ if ( preg_match( '#^/[^/]+/$#', $collection_path) ) {
+ $sql .= "SELECT user_no, '/' || username || '/' AS dav_name, md5( '/' || username || '/') AS dav_etag, ";
+ $sql .= "updated AS created, fullname AS dav_displayname, FALSE AS is_calendar FROM usr WHERE user_no = $user_no ; ";
+ }
+ else {
+ $sql = "SELECT dav_name, dav_etag, created, dav_displayname, is_calendar FROM collection WHERE user_no = $user_no AND dav_name = ".qpg($collection_path);
+ }
+ $qry = new PgQuery($sql );
+ if( $qry->Exec("PROPFIND",__LINE,__FILE__) && $qry->rows > 0 && $collection = $qry->Fetch() ) {
+ $responses[] = collection_to_xml( $collection );
+ }
+ elseif ( $c->collections_always_exist ) {
+ $collection->dav_name = $collection_path;
+ $collection->dav_etag = md5($collection_path);
+ $collection->is_calendar = 't'; // Everything is a calendar, if it always exists!
+ $collection->dav_displayname = $collection_path;
+ $collection->created = date('Ymd"T"His');
+ $responses[] = collection_to_xml( $collection );
+ }
+ }
+ if ( $depth > 0 ) {
+ $responses = array_merge($responses, get_collection_contents( $depth-1, $user_no, $collection_path ) );
+ }
+ return $responses;
+}
+
+
+if ( count($unsupported) > 0 ) {
+
+ /**
+ * That's a *BAD* request!
+ */
+
+ header('HTTP/1.1 403 Forbidden');
+ header('Content-Type: application/xml; charset="utf-8"');
+
+ $badprops = new XMLElement( "prop" );
+ foreach( $unsupported AS $k => $v ) {
+ // Not supported at this point...
+ dbg_error_log("ERROR", " PROPFIND: Support for $v::$k properties is not implemented yet");
+ $badprops->NewElement(strtolower($k),false,array("xmlns" => strtolower($v)));
+ }
+ $error = new XMLElement("error", new XMLElement( "propfind",$badprops), array("xmlns" => "DAV:") );
+// dbg_log_array( "PROPFIND", "ERRORXML", $error, true );
+
+ echo $error->Render(0,'');
+ exit(0);
}
else {
- $responses = array();
+
+ /**
+ * Something that we can handle, at least roughly correctly.
+ */
$url = sprintf("http://%s:%d%s%s", $_SERVER['SERVER_NAME'], $_SERVER['SERVER_PORT'], $_SERVER['SCRIPT_NAME'], $find_path );
$url = $_SERVER['SCRIPT_NAME'] . $find_path ;
$url = preg_replace( '#/$#', '', $url);
- $sql = "SELECT * FROM calendar WHERE user_no = ? AND dav_name ~ ?;";
- if ( $calpath == '' ) {
- $sql = "SELECT user_no, '/' || username || '/' AS dav_name, md5( '/' || username || '/') AS dav_etag, updated AS created FROM usr WHERE user_no = $session->user_no UNION ".$sql;
- }
- $qry = new PgQuery($sql, $session->user_no, '^/'.$username.$calpath );
- $qry->Exec("PROPFIND",__LINE,__FILE__);
-
- while( $calendar = $qry->Fetch() ) {
- $url = $_SERVER['SCRIPT_NAME'] . $calendar->dav_name;
- $resourcetypes = array( new XMLElement("collection") );
- $contentlength = false;
- if ( $calendar->dav_name != "/$username/" ) {
- $resourcetypes[] = new XMLElement("calendar", false, array("xmlns" => "urn:ietf:params:xml:ns:caldav"));
- $lqry = new PgQuery("SELECT sum(length(caldav_data)) FROM caldav_data WHERE user_no = ? AND dav_name ~ ?;", $session->user_no, '^/'.$username.$calpath.'[^/]+$' );
- if ( $lqry->Exec("PROPFIND",__LINE,__FILE__) && $row = $lqry->Fetch() ) {
- $contentlength = $row->sum;
- }
- }
- $prop = new XMLElement("prop");
- if ( isset($attribute_list['GETCONTENTLENGTH']) ) {
- $prop->NewElement("getcontentlength", $contentlength );
- }
- if ( isset($attribute_list['GETCONTENTTYPE']) ) {
-// $prop->NewElement("getcontenttype", "text/calendar" );
- $prop->NewElement("getcontenttype", "httpd/unix-directory" );
- }
- if ( isset($attribute_list['RESOURCETYPE']) ) {
- $prop->NewElement("resourcetype", $resourcetypes );
- }
- if ( isset($attribute_list['GETETAG']) ) {
- $prop->NewElement("getetag", '"'.$calendar->dav_etag.'"' );
- }
- $status = new XMLElement("status", "HTTP/1.1 200 OK" );
-
- $propstat = new XMLElement( "propstat", array( $prop, $status) );
- $href = new XMLElement("href", $url );
-
- $responses[] = new XMLElement( "response", array($href,$propstat));
- }
+ $responses = get_collection( $depth, $session->user_no, $find_path );
$multistatus = new XMLElement( "multistatus", $responses, array('xmlns'=>'DAV:') );
}
-dbg_log_array( "PROPFIND", "XML", $multistatus, true );
+// dbg_log_array( "PROPFIND", "XML", $multistatus, true );
$xmldoc = $multistatus->Render();
$etag = md5($xmldoc);
header("HTTP/1.1 207 Multi-Status");
header("Content-type: text/xml;charset=UTF-8");
-header("DAV: 1, 2, calendar-access, calendar-schedule");
header("ETag: \"$etag\"");
echo''."\n";
diff --git a/inc/caldav-REPORT.php b/inc/caldav-REPORT.php
index ad9fbb51..adab30e7 100644
--- a/inc/caldav-REPORT.php
+++ b/inc/caldav-REPORT.php
@@ -8,7 +8,7 @@ xml_parser_set_option ( $parser, XML_OPTION_SKIP_WHITE, 1 );
function xml_start_callback( $parser, $el_name, $el_attrs ) {
// dbg_error_log( "REPORT", "Parsing $el_name" );
- dbg_log_array( "REPORT", "$el_name::attrs", $el_attrs, true );
+// dbg_log_array( "REPORT", "$el_name::attrs", $el_attrs, true );
$attributes[$el_name] = $el_attrs;
}
@@ -22,16 +22,43 @@ $rpt_request = array();
xml_parse_into_struct( $parser, $raw_post, $rpt_request );
xml_parser_free($parser);
+require_once("XMLElement.php");
+
$reportnum = -1;
$report = array();
foreach( $rpt_request AS $k => $v ) {
- switch ( $v['tag'] ) {
+ $fulltag = $v['tag'];
+ if ( preg_match('/^(.*):([^:]+)$/', $fulltag, $matches) ) {
+ $xmlns = $matches[1];
+ $xmltag = $matches[2];
+ }
+ else {
+ $xmlns = 'DAV:';
+ $xmltag = $tag;
+ }
+
+ switch ( $fulltag ) {
+
+ case 'URN:IETF:PARAMS:XML:NS:CALDAV:CALENDAR-QUERY':
+ dbg_error_log( "PROPFIND", ":Request: %s -> %s", $v['type'], $xmltag );
+ if ( $v['type'] == "open" ) {
+ $reportnum++;
+ $report[$reportnum]['type'] = $xmltag;
+ $report[$reportnum]['include_href'] = 1;
+ $report[$reportnum]['include_data'] = 1;
+ }
+ else {
+ unset($report_type);
+ }
+ break;
case 'URN:IETF:PARAMS:XML:NS:CALDAV:CALENDAR-MULTIGET':
- dbg_log_array( "REPORT", "CALENDAR-MULTIGET", $v, true );
+ dbg_error_log( "PROPFIND", ":Request: %s -> %s", $v['type'], $xmltag );
$report[$reportnum]['multiget'] = 1;
if ( $v['type'] == "open" ) {
+ $reportnum++;
+ $report[$reportnum]['type'] = $xmltag;
$multiget_names = array();
}
else if ( $v['type'] == "close" ) {
@@ -40,24 +67,36 @@ foreach( $rpt_request AS $k => $v ) {
}
break;
- case 'URN:IETF:PARAMS:XML:NS:CALDAV:CALENDAR-DATA':
- dbg_log_array( "REPORT", "CALENDAR-DATA", $v, true );
- if ( $v['type'] == "complete" ) {
- $report[$reportnum]['include_data'] = 1;
+ case 'URN:IETF:PARAMS:XML:NS:CALDAV:FILTER':
+ dbg_error_log( "PROPFIND", ":Request: %s -> %s", $v['type'], $xmltag );
+ if ( $v['type'] == "open" ) {
+ $filters = array();
+ }
+ else if ( $v['type'] == "close" ) {
+ $report[$reportnum]['filters'] = $filters;
+ unset($filters);
}
break;
- case 'URN:IETF:PARAMS:XML:NS:CALDAV:CALENDAR-QUERY':
- dbg_log_array( "REPORT", "CALENDAR-QUERY", $v, true );
- if ( $v['type'] == "open" ) {
- $reportnum++;
- $report_type = substr($v['tag'],30);
- $report[$reportnum]['type'] = $report_type;
- $report[$reportnum]['include_href'] = 1;
- $report[$reportnum]['include_data'] = 1;
+ case 'URN:IETF:PARAMS:XML:NS:CALDAV:IS-DEFINED':
+ case 'URN:IETF:PARAMS:XML:NS:CALDAV:COMP-FILTER':
+ dbg_error_log( "PROPFIND", ":Request: %s -> %s", $v['type'], $xmltag );
+ if ( $v['type'] == "close" ) {
+ break;
+ }
+ if ( $v['type'] == "complete" ) {
+ $filter_name = $xmltag;
}
else {
- unset($report_type);
+ $filter_name = $v['attributes']['NAME'];
+ }
+ dbg_log_array( "REPORT", "COMP-FILTER", $v, true );
+ if ( isset($filters) ) {
+ dbg_error_log( "REPORT", "Adding filter '%s'", $filter_name );
+ $filters[$filter_name] = 1;
+ }
+ else {
+ dbg_error_log( "ERROR", "Not using COMP-FILTER '%s' outside of defined FILTER!", $filter_name );
}
break;
@@ -71,23 +110,6 @@ foreach( $rpt_request AS $k => $v ) {
}
break;
- case 'URN:IETF:PARAMS:XML:NS:CALDAV:COMP-FILTER':
- dbg_log_array( "REPORT", "COMP-FILTER", $v, true );
- if ( isset($v['attributes']['NAME']) && ($v['attributes']['NAME'] == 'VCALENDAR' )) {
- $report[$reportnum]['calendar'] = 1;
- }
- if ( isset($v['attributes']['NAME']) ) {
- if ( isset($report[$reportnum]['calendar']) && ($v['attributes']['NAME'] == 'VEVENT') ) {
- $report[$reportnum]['calendar-event'] = 1;
- }
- if ( isset($report[$reportnum]['calendar']) && ($v['attributes']['NAME'] == 'VTODO') ) {
- $report[$reportnum]['calendar-todo'] = 1;
- }
- if ( isset($report[$reportnum]['calendar']) && ($v['attributes']['NAME'] == 'VFREEBUSY') ) {
- $report[$reportnum]['calendar-freebusy'] = 1;
- }
- }
- break;
case 'URN:IETF:PARAMS:XML:NS:CALDAV:FILTER':
dbg_error_log( "REPORT", "Not using %s information which follows...", $v['tag'] );
@@ -96,7 +118,7 @@ foreach( $rpt_request AS $k => $v ) {
case 'DAV::PROP':
dbg_log_array( "REPORT", "DAV::PROP", $v, true );
- if ( isset($report_type) ) {
+ if ( isset($report[$reportnum]['type']) ) {
if ( $v['type'] == "open" ) {
$report_properties = array();
}
@@ -113,159 +135,187 @@ foreach( $rpt_request AS $k => $v ) {
}
break;
+ case 'URN:IETF:PARAMS:XML:NS:CALDAV:CALENDAR-DATA':
case 'DAV::GETETAG':
case 'DAV::GETCONTENTLENGTH':
case 'DAV::GETCONTENTTYPE':
case 'DAV::RESOURCETYPE':
if ( isset($report_properties) ) {
- $attribute = substr($v['tag'],5);
- $report_properties[$attribute] = 1;
+ dbg_error_log( "REPORT", "Adding property '%s'", $xmltag );
+ $report_properties[$xmltag] = 1;
+ }
+ else {
+ dbg_error_log( "ERROR", "Not using property '%s' outside of defined report!", $xmltag );
}
break;
case 'DAV::HREF':
- dbg_log_array( "REPORT", "DAV::HREF", $v, true );
- if ( isset($report[$reportnum]['multiget']) ) {
- $multiget_names[] = $v['value'];
+ if ( $report[$reportnum]['type'] == 'CALENDAR-MULTIGET' ) {
+ $value = preg_replace( "#^.*".$_SERVER['SCRIPT_NAME']."/#", "/", $v['value'] );
+ $multiget_names[] = $value;
}
+ else {
+ dbg_error_log( "ERROR", "Not using DAV::HREF '%s' for report type '%s'!", $v['value'], $report[$reportnum]['type'] );
+ }
+ break;
default:
- dbg_error_log( "REPORT", "Unhandled tag >>".$v['tag']."<<");
+ $unsupported[$xmltag] = $xmlns;
+ dbg_error_log( "REPORT", "Unhandled tag >>%s<<", $fulltag );
}
}
-if ( $unsupported_stuff ) {
- header('HTTP/1.1 403 Forbidden');
- header('Content-Type: application/xml; charset="utf-8"');
- echo <<
-
-
-
-
-
-EOXML;
- exit(0);
-}
-
-header("HTTP/1.1 207 Multi-Status");
-header("Content-type: text/xml;charset=UTF-8");
/**
-* FIXME - this needs to be rewritten using XML libraries, in the same manner
-* in which the REPORT request is parsed, in fact. For the time being we will
-* attach importance to the care and feeding of Evolution, however.
+* Return XML for a single calendar (or todo) entry from the DB
*/
-$response_tpl = <<%s
-
- %s%s
-
- HTTP/1.1 200 OK
-
-
+function calendar_to_xml( $properties, $item ) {
+ global $session, $c;
-RESPONSETPL;
+ dbg_error_log("PROPFIND","Building XML Response for item '%s'", $item->dav_name );
-$property_tpl = <<dav_name );
+ $prop = new XMLElement("prop");
+ if ( isset($properties['GETCONTENTLENGTH']) ) {
+ $contentlength = strlen($item->caldav_data);
+ $prop->NewElement("getcontentlength", $contentlength );
+ }
+ if ( isset($properties['CALENDAR-DATA']) ) {
+ $prop->NewElement("calendar-data", $item->caldav_data, array("xmlns" => "urn:ietf:params:xml:ns:caldav") );
+ }
+ if ( isset($properties['GETCONTENTTYPE']) ) {
+ $prop->NewElement("getcontenttype", "text/calendar" );
+ }
+ if ( isset($properties['RESOURCETYPE']) ) {
+ $prop->NewElement("resourcetype", new XMLElement("calendar", false, array("xmlns" => "urn:ietf:params:xml:ns:caldav")) );
+ }
+ if ( isset($properties['DISPLAYNAME']) ) {
+ $prop->NewElement("displayname");
+ }
+ if ( isset($properties['GETETAG']) ) {
+ $prop->NewElement("getetag", '"'.$item->dav_etag.'"' );
+ }
+ if ( isset($properties['CURRENT-USER-PRIVILEGE-SET']) ) {
+ /**
+ * FIXME: Fairly basic set of privileges at present.
+ */
+ if ( $session->AllowedTo("Admin") && preg_match("#/.+/.#", $item->dav_name) ) {
+ $privs = array("all");
+ }
+ else {
+ $privs = array("read");
+ if ( $session->user_no == $item->user_no || $session->AllowedTo("Admin") ) {
+ $privs[] = "write";
+ }
+ }
+ $prop->NewElement("current-user-privilege-set", privileges($privs) );
+ }
+ $status = new XMLElement("status", "HTTP/1.1 200 OK" );
- "%s"
-PROPERTYTPL;
+ $propstat = new XMLElement( "propstat", array( $prop, $status) );
+ $href = new XMLElement("href", $url );
-$calendar_href_tpl = <<http://%s:%d%s%s
-CALDATATPL;
+ return $response;
+}
-$calendar_data_tpl = <<%s
-CALDATATPL;
-dbg_log_array("REPORT", "report", $report, true );
+if ( count($unsupported) > 0 ) {
-echo <<
-
+ /**
+ * That's a *BAD* request!
+ */
-REPORTHDR;
+ header('HTTP/1.1 403 Forbidden');
+ header('Content-Type: application/xml; charset="utf-8"');
+
+ $badprops = new XMLElement( "prop" );
+ foreach( $unsupported AS $k => $v ) {
+ dbg_error_log("ERROR", " REPORT: Support for $v::$k properties is not implemented yet");
+ $badprops->NewElement(strtolower($k),false,array("xmlns" => strtolower($v)));
+ }
+ $error = new XMLElement("error", new XMLElement( "propfind",$badprops), array("xmlns" => "DAV:") );
+
+ echo $error->Render(0,'');
+
+ exit(0);
+}
+else {
+
+ /**
+ * Something that we can handle, at least roughly correctly.
+ */
+
+ $responses = array();
for ( $i=0; $i <= $reportnum; $i++ ) {
dbg_error_log("REPORT", "Report[%d] Start:%s, End: %s, Events: %d, Todos: %d, Freebusy: %d",
- $i, $report[$i]['start'], $report[$i]['end'], $report[$i]['calendar-event'], $report[$i]['calendar-todo'], $report[$i]['calendar-freebusy']);
+ $i, $report[$i]['start'], $report[$i]['end'], $report[$i]['filters']['VEVENT'], $report[$i]['filters']['VTODO'], $report[$i]['filters']['VFREEBUSY']);
- if ( isset($report[$i]['calendar-event']) ) {
- /**
- * Produce VEVENT 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 event WHERE caldav_type = 'VEVENT' ";
- $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("REPORT",__LINE__,__FILE__) && $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 ) : "" );
- $properties = "";
- foreach( $report[$i]['properties'] AS $k => $v ) {
- switch( $k ) {
- case 'GETETAG': $value = $event->dav_etag; break;
- case 'GETCONTENTLENGTH': $value = strlen($event->caldav_data); break;
- case 'GETCONTENTTYPE': $value = "text/calendar"; break;
- case 'RESOURCETYPE': $value = "VEVENT"; break;
- }
- $properties .= sprintf( $property_tpl, strtolower($k), $value, strtolower($k));
- }
- printf( $response_tpl, $calhref, $properties, $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);
+ $where = "";
+ switch( $report[$i]['type'] ) {
+ case 'CALENDAR-QUERY':
+ 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 ";
+ }
+ break;
+
+ case 'CALENDAR-MULTIGET':
+ $href_in = '';
+ foreach( $report[$reportnum]['get_names'] AS $k => $v ) {
+ dbg_error_log("REPORT", "Reporting on href '%s'", $v );
+ $href_in .= ($href_in == '' ? '' : ', ');
+ $href_in .= qpg($v);
+ }
+ if ( $href_in != "" ) {
+ $where .= " AND caldav_data.dav_name IN ( $href_in ) ";
+ }
+ break;
+
+ default:
+ dbg_error_log("REPORT", "Unhandled report type of '%s'", $report[$i]['type'] );
}
- if ( isset($report[$i]['calendar-todo']) ) {
- /**
- * 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("REPORT",__LINE__,__FILE__) && $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);
- }
- }
+ $type_filters = '';
+ if ( isset($report[$i]['filters']['VEVENT']) ) {
+ $type_filters .= ($type_filters == '' ? '' : ', ');
+ $type_filters .= qpg('VEVENT');
}
- if ( isset($report[$i]['calendar-freebusy']) ) {
- if ( isset($report[$i]['include_data']) ) dbg_error_log( "REPORT", "FIXME: Not returning full freebusy data" );
+ if ( isset($report[$i]['filters']['VTODO']) ) {
+ $type_filters .= ($type_filters == '' ? '' : ', ');
+ $type_filters .= qpg('VTODO');
+ }
+ if ( $type_filters != '' ) {
+ $where .= " AND caldav_data.caldav_type IN ( $type_filters ) ";
+ }
+ if ( $where != '' ) {
+ $where = preg_replace( '#^\s*(AND|OR) #i', ' WHERE ', $where);
+ }
+
+ $qry = new PgQuery( "SELECT * FROM caldav_data INNER JOIN calendar_item USING(user_no, dav_name)". $where );
+ if ( $qry->Exec("REPORT",__LINE__,__FILE__) && $qry->rows > 0 ) {
+ while( $calendar_object = $qry->Fetch() ) {
+ $responses[] = calendar_to_xml($report[$i]['properties'], $calendar_object );
+ }
}
}
+}
+$multistatus = new XMLElement( "multistatus", $responses, array('xmlns'=>'DAV:') );
-echo <<
-EOXML;
+$xmldoc = $multistatus->Render();
+$etag = md5($xmldoc);
+
+header("HTTP/1.1 207 Multi-Status");
+header("Content-type: text/xml;charset=UTF-8");
+header("ETag: \"$etag\"");
+
+echo''."\n";
+echo $xmldoc;
?>
\ No newline at end of file
diff --git a/rscds.webprj b/rscds.webprj
index 040d5b43..4db406a4 100644
--- a/rscds.webprj
+++ b/rscds.webprj
@@ -12,7 +12,6 @@
-
@@ -45,10 +44,44 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+