diff --git a/inc/CalDAVPrincipal.php b/inc/CalDAVPrincipal.php index 708d043c..f46caba9 100644 --- a/inc/CalDAVPrincipal.php +++ b/inc/CalDAVPrincipal.php @@ -148,6 +148,8 @@ class CalDAVPrincipal foreach( $usr AS $k => $v ) { $this->{$k} = $v; } + if ( !isset($this->modified) ) $this->modified = ISODateToHTTPDate($this->updated); + if ( !isset($this->created) ) $this->created = ISODateToHTTPDate($this->joined); $this->by_email = false; $this->url = ConstructURL( "/".$this->username."/" ); @@ -247,27 +249,20 @@ class CalDAVPrincipal $path_split = explode('/', $path ); @dbg_error_log( "principal", "Path split into at least /// %s /// %s /// %s", $path_split[1], $path_split[2], $path_split[3] ); - if ( substr($path,0,1) == '~' ) { - // URL is for a principal, by name - $username = substr($path_split[1],1); - $user = getUserByID($username); - $user_no = $user->user_no; - } - else { - $username = $path_split[1]; + $username = $path_split[1]; + if ( substr($username,0,1) == '~' ) $username = substr($username,1); - if ( isset($options['allow_by_email']) && preg_match( '#/(\S+@\S+[.]\S+)$#', $path, $matches) ) { - $email = $matches[1]; - $qry = new PgQuery("SELECT user_no, username FROM usr WHERE email = ?;", $email ); - if ( $qry->Exec("principal") && $user = $qry->Fetch() ) { - $user_no = $user->user_no; - $username = $user->username; - } - } - elseif( $user = getUserByName( $username, 'caldav') ) { + if ( isset($options['allow_by_email']) && preg_match( '#/(\S+@\S+[.]\S+)$#', $path, $matches) ) { + $email = $matches[1]; + $qry = new PgQuery("SELECT user_no, username FROM usr WHERE email = ?;", $email ); + if ( $qry->Exec("principal") && $user = $qry->Fetch() ) { $user_no = $user->user_no; + $username = $user->username; } } + elseif( $user = getUserByName( $username, 'caldav') ) { + $user_no = $user->user_no; + } return $username; } @@ -287,6 +282,26 @@ class CalDAVPrincipal } + /** + * Returns a representation of the principal as a collection + */ + function AsCollection() { + $collection = (object) array( + 'collection_id' => (isset($this->collection_id) ? $this->collection_id : 0), + 'is_calendar' => 'f', + 'is_principal' => 't', + 'user_no' => (isset($this->user_no) ? $this->user_no : 0), + 'username' => (isset($this->username) ? $this->username : 0), + 'email' => (isset($this->email) ? $this->email : ''), + 'created' => (isset($this->created) ? $this->created : date('Ymd\THis')) + ); + $collection->dav_name = (isset($this->dav_name) ? $this->dav_name : '/' . $this->username . '/'); + $collection->dav_etag = (isset($this->dav_etag) ? $this->dav_etag : md5($this->username . $this->updated)); + $collection->dav_displayname = (isset($this->dav_displayname) ? $this->dav_displayname : (isset($this->fullname) ? $this->fullname : $this->username)); + return $collection; + } + + /** * Returns the array of privilege names converted into XMLElements */ @@ -336,27 +351,19 @@ class CalDAVPrincipal break; case 'DAV::getlastmodified': - $prop->NewElement("getlastmodified", ISODateToHTTPDate($this->updated) ); + $prop->NewElement("getlastmodified", $this->modified ); break; case 'DAV::creationdate': - $prop->NewElement("creationdate", ISODateToHTTPDate($this->joined) ); + $prop->NewElement("creationdate", $this->created ); break; case 'DAV::group-member-set': - $set = array(); - foreach( $this->group_member_set AS $k => $url ) { - $set[] = $reply->href($url ); - } - $prop->NewElement("group-member-set", $set ); + $prop->NewElement("group-member-set", $reply->href($this->group_member_set) ); break; case 'DAV::group-membership': - $set = array(); - foreach( $this->group_membership AS $k => $url ) { - $set[] = $reply->href($url ); - } - $prop->NewElement("group-membership", $set ); + $prop->NewElement("group-membership", $reply->href($this->group_membership) ); break; case 'urn:ietf:params:xml:ns:caldav:schedule-inbox-URL': @@ -376,11 +383,7 @@ class CalDAVPrincipal break; case 'urn:ietf:params:xml:ns:caldav:calendar-home-set': - $set = array(); - foreach( $this->calendar_home_set AS $k => $url ) { - $set[] = $reply->href( $url ); - } - $reply->CalDAVElement($prop, "calendar-home-set", $set ); + $reply->CalDAVElement($prop, "calendar-home-set", $reply->href( $this->calendar_home_set ) ); break; case 'urn:ietf:params:xml:ns:caldav:calendar-user-address-set': diff --git a/inc/CalDAVRequest.php b/inc/CalDAVRequest.php index 745a08a0..89e05637 100644 --- a/inc/CalDAVRequest.php +++ b/inc/CalDAVRequest.php @@ -177,6 +177,7 @@ class CalDAVRequest if ( preg_match( $bad_chars_regex, $this->path ) ) { $this->DoResponse( 400, translate("The calendar path contains illegal characters.") ); } + if ( strstr($this->path,'//') ) $this->path = preg_replace( '#//+#', '/', $this->path); $this->user_no = $session->user_no; $this->username = $session->username; @@ -234,10 +235,23 @@ EOSQL; $this->collection = $row; } } - else if ( preg_match( '#/(\S+@\S+[.]\S+)/?$#', $this->path) ) { + else if ( preg_match( '#^((/[^/]+/)calendar-proxy-(read|write))/?[^/]*$#', $this->path, $matches ) ) { + $this->collection_type = 'proxy'; + $this->_is_proxy_request = true; + $this->proxy_type = $matches[3]; + $this->collection_path = $matches[1].'/'; // Enforce trailling '/' + if ( $this->collection_path == $this->path."/" ) { + $this->path .= '/'; + dbg_error_log( "caldav", "Path is actually a (proxy) collection - sending Content-Location header." ); + header( "Content-Location: ".ConstructURL($this->path) ); + } + } + else if ( preg_match( '#^/(\S+@\S+[.]\S+)/?$#', $this->path) ) { + /** @TODO: we should deprecate this now that Evolution 2.27 can do scheduling extensions */ $this->collection_id = -1; $this->collection_type = 'email'; $this->collection_path = $this->path; + $this->_is_principal = true; } else if ( preg_match( '#^(/[^/]+)/?$#', $this->path, $matches) ) { $this->collection_id = -1; @@ -267,6 +281,38 @@ EOSQL; if ( isset($this->principal->username)) $this->username = $this->principal->username; if ( isset($this->principal->by_email)) $this->by_email = true; + if ( $this->collection_type == 'principal' || $this->collection_type == 'email' ) { + $this->collection = $this->principal->AsCollection(); + } + elseif( $this->collection_type == 'root' ) { + $this->collection = (object) array( + 'collection_id' => 0, + 'dav_name' => '/', + 'dav_etag' => md5($c->system_name), + 'is_calendar' => 'f', + 'is_principal' => 'f', + 'user_no' => 0, + 'dav_displayname' => $c->system_name, + 'created' => date('Ymd\THis') + ); + } + elseif( $this->collection_type == 'proxy' ) { + $this->collection = (object) array( + 'dav_name' => $this->collection_path, + 'is_calendar' => 'f', + 'is_principal' => 't', + 'is_proxy' => 't', + 'proxy_type' => $this->proxy_type, + 'dav_displayname' => sprintf('Proxy %s for %s', $this->proxy_type, $this->principal->username), + 'collection_id' => 0, + 'user_no' => $this->principal->user_no, + 'username' => $this->principal->username, + 'email' => $this->principal->email, + 'created' => $this->principal->created, + 'dav_etag' => $this->principal->created + ); + } + /** * Evaluate our permissions for accessing the target */ @@ -582,6 +628,17 @@ EOSQL; } + /** + * Returns true if the URL referenced by this request is within a proxy URL + */ + function IsProxyRequest( ) { + if ( !isset($this->_is_proxy_request) ) { + $this->_is_proxy_request = preg_match( '#^/[^/]+/calendar-proxy-(read|write)/?[^/]*$#', $this->path ); + } + return $this->_is_proxy_request; + } + + /** * Returns true if the request asked for infinite depth */ diff --git a/inc/caldav-PROPFIND.php b/inc/caldav-PROPFIND.php index c77c61b5..d9041c6a 100644 --- a/inc/caldav-PROPFIND.php +++ b/inc/caldav-PROPFIND.php @@ -70,13 +70,13 @@ foreach( $request->xml_tags AS $k => $v ) { case 'urn:ietf:params:xml:ns:caldav:schedule-inbox-URL': /** Support in development */ case 'urn:ietf:params:xml:ns:caldav:schedule-outbox-URL': /** Support in development */ case 'urn:ietf:params:xml:ns:caldav:calendar-free-busy-set': /** Deprecated, but should work fine */ - case 'urn:ietf:params:xml:ns:caldav:supported-calendar-component-set': + case 'urn:ietf:params:xml:ns:caldav:supported-calendar-component-set': /** Should work fine */ /** * Handled calendarserver properties */ case 'http://calendarserver.org/ns/:getctag': /** Calendar Server extension like etag - should work fine (we just return etag) */ - case 'http://calendarserver.org/ns/:calendar-proxy-read-for': /** Calendar Server Delegation readonly */ + case 'http://calendarserver.org/ns/:calendar-proxy-read-for': /** Calendar Server Delegation readonly */ case 'http://calendarserver.org/ns/:calendar-proxy-write-for': /** Calendar Server Delegation read-write */ case 'http://calendarserver.org/ns/:dropbox-home-URL': case 'http://calendarserver.org/ns/:notifications-URL': @@ -202,10 +202,6 @@ function add_principal_properties( &$prop, &$denied ) { $reply->DAVElement( $prop, "alternate-URI-set" ); // Empty - there are no alternatives! } - if (isset($prop_list['DAV::group-membership'])) { - $reply->DAVElement($prop, "group-membership", href_set_from_paths( $request->principal->group_membership )); - } - if ( isset($prop_list['urn:ietf:params:xml:ns:caldav:calendar-home-set'] ) ) { $reply->CalDAVElement( $prop, "calendar-home-set", href_set_from_paths( $request->principal->calendar_home_set ) ); } @@ -223,14 +219,6 @@ function add_principal_properties( &$prop, &$denied ) { $reply->CalendarserverElement($prop, "notifications-URL", $reply->href( $request->principal->notifications_url) ); } - // Caldav proxy (not described in rfc, but CalendarServer has it) - if ( isset($prop_list['http://calendarserver.org/ns/:calendar-proxy-read-for'] ) ) { - $reply->CalendarserverElement($prop, "calendar-proxy-read-for", href_set_from_paths( $request->principal->read_proxy_for ) ); - } - if ( isset($prop_list['http://calendarserver.org/ns/:calendar-proxy-write-for'] ) ) { - $reply->CalendarserverElement($prop, "calendar-proxy-write-for", href_set_from_paths( $request->principal->write_proxy_for ) ); - } - if ( isset($prop_list['urn:ietf:params:xml:ns:caldav:calendar-user-address-set'] ) ) { $reply->CalDAVElement( $prop, "calendar-user-address-set", href_set_from_paths( $request->principal->user_address_set ) ); } @@ -374,18 +362,23 @@ function add_proxy_response( &$responses, $which, $parent_path ) { } else if ( $which == "write" ) { $proxy_group = $request->principal->write_proxy_group; } - if ( !isset($proxy_group) || !is_array($proxy_group) || count($proxy_group) < 1 ) { + if ($parent_path != '/'.$request->principal->username.'/') { return; // Nothing to proxy for } $collection->dav_name = $parent_path."calendar-proxy-".$which."/"; - $collection->is_calendar = 'f'; + $collection->is_calendar = 'f'; $collection->is_principal = 't'; + $collection->is_proxy = 't'; + $collection->proxy_type = $which; $collection->dav_displayname = $collection->dav_name; $collection->collection_id = 0; $collection->user_no = $session->user_no; + $collection->username = $session->username; + $collection->email = $session->email; $collection->created = date('Ymd\THis'); $collection->dav_etag = md5($c->system_name . $collection->dav_name . implode($proxy_group) ); + $collection->proxy_for = $proxy_group; $responses[] = collection_to_xml( $collection ); } @@ -458,9 +451,9 @@ function collection_to_xml( $collection ) { $resourcetypes[] = $reply->NewXMLElement( "principal", false, false, 'DAV:'); } - // As per Caldav Proxy 5.1 par. 3 - if (preg_match('#(calendar-proxy-(read|write))#', $collection->dav_displayname, $matches) && isset($prop_list['DAV::resourcetype'])) { - $resourcetypes[] = $reply->NewXMLElement($matches[1], false, false, 'http://calendarserver.org/ns/'); + if ( isset($collection->is_proxy) && $collection->is_proxy == 't' ) { + // As per Caldav Proxy 5.1 par. 3 + $resourcetypes[] = $reply->NewXMLElement('calendar-proxy-'.$collection->proxy_type, false, false, 'http://calendarserver.org/ns/'); } if ( $allprop || isset($prop_list['DAV::getcontentlength']) ) { @@ -471,6 +464,33 @@ function collection_to_xml( $collection ) { } } + if ( isset($collection->is_proxy) && $collection->is_proxy == 't' ) { + // Caldav proxy (not described in rfc, but CalendarServer has it) + if ( isset($prop_list['http://calendarserver.org/ns/:calendar-proxy-'.$collection->proxy_type.'-for'] ) ) { + if ( $collection->proxy_type == "read" ) { + $proxy_group = $request->principal->read_proxy_for; + } else if ( $collection->proxy_type == "write" ) { + $proxy_group = $request->principal->write_proxy_for; + } + $reply->CalendarserverElement($prop, 'calendar-proxy-'.$collection->proxy_type.'-for', $reply->href( $proxy_group ) ); + } + + if ( isset($prop_list['DAV::group-member-set']) ) { + if ( $collection->proxy_type == "read" ) { + $proxy_group = $request->principal->read_proxy_group; + } else if ( $collection->proxy_type == "write" ) { + $proxy_group = $request->principal->write_proxy_group; + } + $reply->DAVElement($prop, "group-member-set", $reply->href( $proxy_group ) ); + } + + if (isset($prop_list['DAV::group-membership'])) { + $reply->DAVElement($prop, "group-membership", $reply->href( $request->principal->group_membership )); + } + + } + + if ( $allprop || isset($prop_list['DAV::displayname']) ) { $displayname = ( $collection->dav_displayname == "" ? ucfirst(trim(str_replace("/"," ", $collection->dav_name))) : $collection->dav_displayname ); $reply->DAVElement( $prop, "displayname", $displayname ); @@ -578,8 +598,8 @@ function get_collection_contents( $depth, $user_no, $collection ) { * Calendar collections may not contain calendar collections. */ if ( $collection->dav_name == '/' ) { - $sql = "SELECT usr.*, '/' || username || '/' AS dav_name, md5( '/' || username || '/') AS dav_etag, "; - $sql .= "to_char(updated at time zone 'GMT',?) AS created, "; + $sql = "SELECT usr.*, '/' || username || '/' AS dav_name, md5(username || updated::text) AS dav_etag, "; + $sql .= "to_char(joined at time zone 'GMT',?) AS created, "; $sql .= "to_char(updated at time zone 'GMT',?) AS modified, "; $sql .= "fullname AS dav_displayname, FALSE AS is_calendar, TRUE AS is_principal, "; $sql .= "0 AS collection_id "; @@ -613,6 +633,12 @@ function get_collection_contents( $depth, $user_no, $collection ) { } } } + if ( $collection->is_principal == "t" ) { + // Caldav Proxy: 5.1 par. 2: Add child resources calendar-proxy-(read|write) + dbg_error_log("PROPFIND","Adding calendar-proxy-read and write. Path: %s", $collection->dav_name); + add_proxy_response($responses, "read", $collection->dav_name); + add_proxy_response($responses, "write", $collection->dav_name); + } } /** @@ -674,8 +700,8 @@ function get_collection( $depth, $user_no, $collection_path ) { else { $user_no = intval($user_no); if ( preg_match( '#^/[^/]+/$#', $collection_path) ) { - $sql = "SELECT usr.*, '/' || username || '/' AS dav_name, md5( '/' || username || '/') AS dav_etag, "; - $sql .= "to_char(updated at time zone 'GMT',?) AS created, "; + $sql = "SELECT usr.*, '/' || username || '/' AS dav_name, md5( username || updated::text ) AS dav_etag, "; + $sql .= "to_char(joined at time zone 'GMT',?) AS created, "; $sql .= "to_char(updated at time zone 'GMT',?) AS modified, "; $sql .= "fullname AS dav_displayname, FALSE AS is_calendar, TRUE AS is_principal, 0 AS collection_id "; $sql .= "FROM usr WHERE user_no = $user_no "; @@ -700,13 +726,6 @@ function get_collection( $depth, $user_no, $collection_path ) { $responses[] = collection_to_xml( $collection ); } - // Caldav Proxy: 5.1 par. 2: Add child resources calendar-proxy-(read|write) - if (($collection->is_principal && isset($prop_list['DAV::resourcetype'])) ) { // atm, only users/resources/groups are principals, so it's ok to add these. - // this is added when // is queried for resourcetype - dbg_error_log("PROPFIND","Adding calendar-proxy-read and write. Path: %s", $collection->dav_name); - add_proxy_response($responses, "read", $collection->dav_name); - add_proxy_response($responses, "write", $collection->dav_name); - } } elseif ( $c->collections_always_exist && preg_match( "#^/$session->username/#", $collection_path) ) { dbg_error_log("PROPFIND","Using $c->collections_always_exist setting is deprecated" ); @@ -763,7 +782,19 @@ $request->UnsupportedRequest($unsupported); // Won't return if there was unsuppo */ $url = ConstructURL( $request->path ); $url = preg_replace( '#/$#', '', $url); -if ( $request->IsCollection() ) { +$responses = array(); +if ( $request->IsPrincipal() ) { + $responses[] = $request->principal->RenderAsXML(array_merge($prop_list,$arbitrary), &$reply); + if ( $request->depth > 0 ) { + $collection = (object) array( 'dav_name' => '/'.$request->username.'/', 'is_calendar' => 'f', 'is_principal' => 't' ); + $responses = array_merge($responses, get_collection_contents( $request->depth - 1, $request->user_no, $collection ) ); + } +} +if ( $request->IsProxyRequest() ) { + add_proxy_response($responses, $request->proxy_type, '/' . $request->principal->username . '/' ); + /** Nothing inside these, as yet. */ +} +elseif ( $request->IsCollection() ) { $responses = get_collection( $request->depth, $request->user_no, $request->path ); } elseif ( $request->AllowedTo('read') ) {