From 0ed02b6c2e25a518c907bfe3e260f7db15cd818d Mon Sep 17 00:00:00 2001 From: Andrew McMillan Date: Thu, 5 Oct 2006 00:37:25 +1300 Subject: [PATCH] Rewrite of REPORT handling to use XML library and generally much improve the structure of it. Major hacking on PROPFIND so that we can now work with Mulberry. Join todo & event tables into calendar_item table because having them separate was getting very silly. Remove XMLElement.php which is now in the standard libraries. --- config/config.php | 4 +- dba/rscds.sql | 65 ++----- inc/XMLElement.php | 125 ------------- inc/caldav-MKCALENDAR.php | 64 ++++++- inc/caldav-PROPFIND.php | 359 ++++++++++++++++++++++++++++---------- inc/caldav-REPORT.php | 348 ++++++++++++++++++++---------------- rscds.webprj | 37 +++- 7 files changed, 580 insertions(+), 422 deletions(-) delete mode 100644 inc/XMLElement.php 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 .= '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 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +