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.
This commit is contained in:
Andrew McMillan 2006-10-05 00:37:25 +13:00
parent ddf01af76b
commit 0ed02b6c2e
7 changed files with 580 additions and 422 deletions

View File

@ -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;
?>

View File

@ -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.

View File

@ -1,125 +0,0 @@
<?php
/**
* A class to assist with construction of XML documents
*
* @package awl
* @subpackage XMLElement
* @author Andrew McMillan <andrew@catalyst.net.nz>
* @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;
}
}
?>

View File

@ -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...
*
<?xml version="1.0" encoding="utf-8" ?>
<C:mkcalendar xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:set>
<D:prop>
<D:displayname>Lisa's Events</D:displayname>
<C:calendar-description xml:lang="en">Calendar restricted to events.</C:calendar-description>
<C:supported-calendar-component-set>
<C:comp name="VEVENT"/>
</C:supported-calendar-component-set>
<C:calendar-timezone><![CDATA[BEGIN:VCALENDAR
PRODID:-//Example Corp.//CalDAV Client//EN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:US-Eastern
LAST-MODIFIED:19870101T000000Z
BEGIN:STANDARD
DTSTART:19671029T020000
RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
TZOFFSETFROM:-0400
TZOFFSETTO:-0500
TZNAME:Eastern Standard Time (US & Canada)
END:STANDARD
BEGIN:DAYLIGHT
DTSTART:19870405T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
TZOFFSETFROM:-0500
TZOFFSETTO:-0400
TZNAME:Eastern Daylight Time (US & Canada)
END:DAYLIGHT
END:VTIMEZONE
END:VCALENDAR
]]></C:calendar-timezone>
</D:prop>
</D:set>
</C:mkcalendar>
*/
?>

View File

@ -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.
* <?xml version='1.0' encoding='UTF-8'?>
* <multistatus xmlns='DAV:'>
* <response>
* <href>/caldav.php/path/they/sent/</href>
* <propstat>
* <prop>
* <getcontentlength/>
* <getcontenttype>httpd/unix-directory</getcontenttype>
* <resourcetype>
* <collection/>
* </resourcetype>
* </prop>
* <status>HTTP/1.1 200 OK</status>
* </propstat>
* </response>
* <response>
* <href>/caldav.php/path/they/sent/calendar</href>
* <propstat>
* <prop>
* <getcontentlength/>
* <getcontenttype>httpd/unix-directory</getcontenttype>
* <resourcetype>
* <collection/>
* <calendar xmlns='urn:ietf:params:xml:ns:caldav'/>
* </resourcetype>
* </prop>
* <status>HTTP/1.1 200 OK</status>
* </propstat>
* </response>
* </multistatus>
* 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,'<?xml version="1.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'<?xml version="1.0" encoding="UTF-8" ?>'."\n";

View File

@ -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
<?xml version="1.0" encoding="utf-8" ?>
<D:error xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<C:supported-filter>
<C:prop-filter name="X-ABC-GUID"/>
</C:supported-filter>
</D:error>
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 = <<<RESPONSETPL
<D:response>%s
<D:propstat>
<D:prop>%s%s
</D:prop>
<D:status>HTTP/1.1 200 OK</D:status>
</D:propstat>
</D:response>
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 = <<<PROPERTYTPL
$url = sprintf( "%s://%s:%d%s%s", 'http', $_SERVER['SERVER_NAME'], $_SERVER['SERVER_PORT'], $_SERVER['SCRIPT_NAME'], $item->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" );
<D:%s>"%s"</D:%s>
PROPERTYTPL;
$propstat = new XMLElement( "propstat", array( $prop, $status) );
$href = new XMLElement("href", $url );
$calendar_href_tpl = <<<CALDATATPL
$response = new XMLElement( "response", array($href,$propstat));
<D:href>http://%s:%d%s%s</D:href>
CALDATATPL;
return $response;
}
$calendar_data_tpl = <<<CALDATATPL
<C:calendar-data>%s </C:calendar-data>
CALDATATPL;
dbg_log_array("REPORT", "report", $report, true );
if ( count($unsupported) > 0 ) {
echo <<<REPORTHDR
<?xml version="1.0" encoding="utf-8" ?>
<D:multistatus xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
/**
* 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,'<?xml version="1.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
</D:multistatus>
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'<?xml version="1.0" encoding="UTF-8" ?>'."\n";
echo $xmldoc;
?>

View File

@ -12,7 +12,6 @@
<annotations/>
<item url="inc/caldav-OPTIONS.php" uploadstatus="1" />
<item url="inc/" uploadstatus="1" />
<item url="inc/caldav-REPORT.php" uploadstatus="1" />
<item url="inc/always.php" uploadstatus="1" />
<item url="config/config.php" uploadstatus="1" />
<item url="config/" uploadstatus="1" />
@ -45,10 +44,44 @@
<item url="inc/RSCDSUser.php" uploadstatus="1" />
<item url="inc/caldav-PROPFIND.php" uploadstatus="1" />
<item url="inc/caldav-MKCALENDAR.php" uploadstatus="1" />
<item url="inc/XMLElement.php" uploadstatus="1" />
<item url="htdocs/js/" uploadstatus="1" />
<item url="htdocs/js/browse.js" uploadstatus="1" />
<item url="htdocs/css/browse.css" uploadstatus="1" />
<item url="htdocs/css/" uploadstatus="1" />
<item url="inc/caldav-MKCOL.php" uploadstatus="1" />
<item url="testing/" uploadstatus="1" />
<item url="testing/dav_test" uploadstatus="1" />
<item url="testing/tests/chandler/" uploadstatus="1" />
<item url="testing/tests/" uploadstatus="1" />
<item url="testing/tests/chandler/PROPFIND.data" uploadstatus="1" />
<item url="testing/tests/chandler/PROPFIND.test" uploadstatus="1" />
<item url="testing/tests/chandler/PROPFIND1.test" uploadstatus="1" />
<item url="testing/tests/chandler/PROPFIND2.test" uploadstatus="1" />
<item url="testing/tests/chandler/privilege.data" uploadstatus="1" />
<item url="testing/tests/chandler/privilege.test" uploadstatus="1" />
<item url="inc/xxxXMLElement.php" uploadstatus="1" />
<item url="testing/tests/chandler/ticketdiscovery.test" uploadstatus="1" />
<item url="testing/tests/chandler/ticketdiscovery.data" uploadstatus="1" />
<item url="testing/tests/chandler/OPTIONS-top.test" uploadstatus="1" />
<item url="testing/tests/chandler/OPTIONS-dotchandler.test" uploadstatus="1" />
<item url="testing/tests/chandler/PROPFIND-dotchandler.test" uploadstatus="1" />
<item url="testing/tests/evolution/OPTIONS.test" uploadstatus="1" />
<item url="testing/tests/evolution/" uploadstatus="1" />
<item url="testing/tests/chandler/REPORT.test" uploadstatus="1" />
<item url="testing/tests/mulberry/" uploadstatus="1" />
<item url="testing/tests/mulberry/PROPFIND-mulberry.data" uploadstatus="1" />
<item url="testing/tests/mulberry/PROPFIND-mulberry.test" uploadstatus="1" />
<item url="testing/tests/mulberry/PROPFIND-initial.test" uploadstatus="1" />
<item url="testing/tests/mulberry/PROPFIND-initial.data" uploadstatus="1" />
<item url="testing/tests/mulberry/REPORT-1href.data" uploadstatus="1" />
<item url="testing/tests/evolution/REPORT.test" uploadstatus="1" />
<item url="testing/tests/evolution/REPORT.data" uploadstatus="1" />
<item url="testing/tests/mulberry/REPORT-1href.test" uploadstatus="1" />
<item url="inc/xxx-caldav-REPORT.php" uploadstatus="1" />
<item url="inc/caldav-REPORT.php" uploadstatus="1" />
<item url="testing/tests/mulberry/PUT1.data" />
<item url="testing/tests/mulberry/PUT2.data" />
<item url="testing/tests/mulberry/PUT2.test" />
<item url="testing/tests/mulberry/PUT1.test" />
</project>
</webproject>