* @copyright Catalyst .Net Ltd * @license http://gnu.org/copyleft/gpl.html GNU GPL v2 */ dbg_error_log("PROPFIND", "method handler"); if ( ! isset($permissions['read']) ) { header("HTTP/1.1 403 Forbidden"); header("Content-type: text/plain"); echo "You may not access that calendar."; dbg_error_log("GET", "Access denied for User: %d, Path: %s", $session->user_no, $request_path); return; } require_once("XMLElement.php"); require_once("iCalendar.php"); $href_list = array(); $attribute_list = array(); $unsupported = array(); foreach( $xml_tags AS $k => $v ) { $tag = $v['tag']; switch ( $tag ) { case 'DAV::PROPFIND': case 'DAV::PROP': dbg_error_log( "PROPFIND", ":Request: %s -> %s", $v['type'], $tag ); break; case 'HTTP://APACHE.ORG/DAV/PROPS/:EXECUTABLE': case 'DAV::ACL': case 'DAV::CHECKED-OUT': case 'DAV::CHECKED-IN': case 'DAV::GETLASTMODIFIED': case 'DAV::GETETAG': case 'DAV::DISPLAYNAME': case 'DAV::GETCONTENTLENGTH': case 'DAV::GETCONTENTTYPE': case 'DAV::RESOURCETYPE': case 'DAV::SUPPORTED-PRIVILEGE-SET': 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 ); $href_list[] = $v['value']; default: if ( preg_match('/^(.*):([^:]+)$/', $tag, $matches) ) { $unsupported[$matches[2]] = $matches[1]; } else { $unsupported[$tag] = ""; } dbg_error_log( "PROPFIND", "Unhandled tag >>%s<<", $tag); } } /** * Returns the array of privilege names converted into XMLElements */ function privileges($privilege_names, $container="privilege") { $privileges = array(); foreach( $privilege_names AS $k => $v ) { $privileges[] = new XMLElement($container, 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 ~ ?;", $collection->user_no, $collection->dav_name.'[^/]+$' ); if ( $lqry->Exec("PROPFIND",__LINE__,__FILE__) && $row = $lqry->Fetch() ) { $contentlength = $row->sum; } } $prop = new XMLElement("prop"); if ( isset($attribute_list['GETLASTMODIFIED']) ) { $prop->NewElement("getlastmodified", ( isset($collection->modified)? $collection->modified : false )); } 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']) ) { $prop->NewElement("current-user-privilege-set", privileges($GLOBALS['permissions']) ); } if ( isset($attribute_list['ACL']) ) { /** * FIXME: This information is semantically valid but presents an incorrect picture. */ $principal = new XMLElement("principal"); $principal->NewElement("authenticated"); $grant = new XMLElement( "grant", array(privileges($GLOBALS['permissions'])) ); $prop->NewElement("acl", new XMLElement( "ace", array( $principal, $grant ) ) ); } if ( isset($attribute_list['SUPPORTED-PRIVILEGE-SET']) ) { /** * FIXME: This information is semantically valid and is correct, but could be extended * if we allow clients such as Mulberry to manipulate these values. */ $prop->NewElement("supported-privilege-set", privileges(array("read","write"), "supported-privilege") ); } $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; } /** * Return XML for a single data item from the DB */ function item_to_xml( $item ) { global $attribute_list, $session, $c; dbg_error_log("PROPFIND","Building XML Response for item '%s'", $item->dav_name ); $url = $_SERVER['SCRIPT_NAME'] . $item->dav_name; $prop = new XMLElement("prop"); if ( isset($attribute_list['GETLASTMODIFIED']) ) { $prop->NewElement("getlastmodified", ( isset($item->modified)? $item->modified : false )); } 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']) ) { $prop->NewElement("current-user-privilege-set", privileges($GLOBALS['permissions']) ); } $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 ) { global $session; dbg_error_log("PROPFIND","Getting collection contents: Depth %d, User: %d, Path: %s", $depth, $user_no, $collection->dav_name ); $responses = array(); if ( $collection->is_calendar != 't' ) { /** * Calendar collections may not contain calendar collections. */ if ( $collection->dav_name == '/' ) { $sql = "SELECT user_no, user_no, '/' || username || '/' AS dav_name, md5( '/' || username || '/') AS dav_etag, "; $sql .= "updated AS created, to_char(updated at time zone 'GMT',?) AS modified, fullname AS dav_displayname, FALSE AS is_calendar FROM usr"; } else { $sql = "SELECT user_no, dav_name, dav_etag, created, to_char(modified at time zone 'GMT',?), dav_displayname, is_calendar FROM collection WHERE parent_container=".qpg($collection->dav_name); } $qry = new PgQuery($sql, PgQuery::Plain(iCalendar::HttpDateFormat())); if( $qry->Exec("PROPFIND",__LINE__,__FILE__) && $qry->rows > 0 ) { while( $subcollection = $qry->Fetch() ) { $responses[] = collection_to_xml( $subcollection ); if ( $depth > 0 ) { $responses = array_merge( $responses, get_collection( $depth - 1, $user_no, $subcollection->dav_name ) ); } } } } dbg_error_log("PROPFIND","Getting collection items: Depth %d, User: %d, Path: %s", $depth, $user_no, $collection->dav_name ); $sql = "SELECT dav_name, caldav_data, dav_etag, created, to_char(modified at time zone 'GMT',?) FROM caldav_data WHERE dav_name ~ ".qpg('^'.$collection->dav_name.'[^/]+$'); $qry = new PgQuery($sql, PgQuery::Plain(iCalendar::HttpDateFormat())); 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 user_no, 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 && isset($collection) ) { $responses = array_merge($responses, get_collection_contents( $depth-1, $user_no, $collection ) ); } 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); } elseif ( isset($permissions['read']) || isset($permissions['write']) ) { /** * 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'], $request_path ); $url = $_SERVER['SCRIPT_NAME'] . $request_path ; $url = preg_replace( '#/$#', '', $url); $responses = get_collection( $query_depth, (isset($path_user_no) ? $path_user_no : $session->user_no), $request_path ); $multistatus = new XMLElement( "multistatus", $responses, array('xmlns'=>'DAV:') ); } else { header('HTTP/1.1 403 Forbidden'); header('Content-Type: text/plain'); echo "You do not have appropriate rights to view that resource\n"; dbg_log_array("caldav","PERMISSIONS", $permissions, true ); exit(0); } // 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("ETag: \"$etag\""); echo''."\n"; echo $xmldoc; ?>