From 24a897108092184ebfdee110a677c06b83333fe4 Mon Sep 17 00:00:00 2001 From: Andrew McMillan Date: Tue, 13 Oct 2009 21:29:40 +1300 Subject: [PATCH] Initial support for draft-daboo-webdav-sync-01 --- dba/appuser_permissions.txt | 1 + dba/patches/1.2.6.sql | 383 +++++++++++++++++- dba/patches/1.2.7.sql | 26 +- inc/CalDAVRequest.php | 8 + inc/DAVResource.php | 123 +++--- inc/caldav-DELETE.php | 13 +- inc/caldav-REPORT-sync-collection.php | 46 ++- .../099-REPORT-sync-initial.result | 21 + .../099-REPORT-sync-initial.test | 19 + .../598-REPORT-sync-initial.result | 161 ++++++++ .../598-REPORT-sync-initial.test | 19 + .../599-REPORT-sync-changed.result | 161 ++++++++ .../599-REPORT-sync-changed.test | 19 + 13 files changed, 917 insertions(+), 83 deletions(-) create mode 100644 testing/tests/regression-suite/099-REPORT-sync-initial.result create mode 100644 testing/tests/regression-suite/099-REPORT-sync-initial.test create mode 100644 testing/tests/regression-suite/598-REPORT-sync-initial.result create mode 100644 testing/tests/regression-suite/598-REPORT-sync-initial.test create mode 100644 testing/tests/regression-suite/599-REPORT-sync-changed.result create mode 100644 testing/tests/regression-suite/599-REPORT-sync-changed.test diff --git a/dba/appuser_permissions.txt b/dba/appuser_permissions.txt index ed46f1fc..71cf4842 100644 --- a/dba/appuser_permissions.txt +++ b/dba/appuser_permissions.txt @@ -44,6 +44,7 @@ GRANT SELECT,UPDATE ON session_session_id_seq ON principal_principal_id_seq ON principal_type_principal_type_id_seq + ON sync_tokens_sync_token_seq GRANT SELECT,INSERT ON time_zone diff --git a/dba/patches/1.2.6.sql b/dba/patches/1.2.6.sql index ca22d538..a782b3fd 100644 --- a/dba/patches/1.2.6.sql +++ b/dba/patches/1.2.6.sql @@ -3,10 +3,391 @@ -- as an integer to make calculation of merged permissions simpler -- through simple binary 'AND' +CREATE or REPLACE FUNCTION legacy_privilege_to_bits( TEXT ) RETURNS BIT(24) AS $$ +DECLARE + in_priv ALIAS FOR $1; + out_bits BIT(24); +BEGIN + out_bits := 0::BIT(24); + IF in_priv ~* 'A' THEN + out_bits = ~ out_bits; + RETURN out_bits; + END IF; + + -- The CALDAV:read-free-busy privilege MUST be aggregated in the DAV:read privilege. + -- 1 DAV:read + -- 512 CalDAV:read-free-busy + -- 4096 CALDAV:schedule-query-freebusy + IF in_priv ~* 'R' THEN + out_bits := out_bits | 4609::BIT(24); + END IF; + + -- DAV:write => DAV:write MUST contain DAV:bind, DAV:unbind, DAV:write-properties and DAV:write-content + -- 2 DAV:write-properties + -- 4 DAV:write-content + -- 64 DAV:bind + -- 128 DAV:unbind + IF in_priv ~* 'W' THEN + out_bits := out_bits | 198::BIT(24); + END IF; + + -- 64 DAV:bind + IF in_priv ~* 'B' THEN + out_bits := out_bits | 64::BIT(24); + END IF; + + -- 128 DAV:unbind + IF in_priv ~* 'U' THEN + out_bits := out_bits | 128::BIT(24); + END IF; + + -- 512 CalDAV:read-free-busy + -- 4096 CALDAV:schedule-query-freebusy + IF in_priv ~* 'F' THEN + out_bits := out_bits | 4608::BIT(24); + END IF; + + RETURN out_bits; +END +$$ +LANGUAGE 'PlPgSQL' IMMUTABLE STRICT; + +-- This legacy conversion function will eventually be removed, once all logic +-- has been converted to use bitmaps, or to use the bits_to_priv() output. +-- +-- NOTE: Round-trip through this and then back through legacy_privilege_to_bits +-- function is lossy! Through legacy_privilege_to_bits() and back through +-- this one is not. +-- +CREATE or REPLACE FUNCTION bits_to_legacy_privilege( BIT(24) ) RETURNS TEXT AS $$ +DECLARE + in_bits ALIAS FOR $1; + out_priv TEXT; +BEGIN + out_priv := ''; + IF in_bits = (~ 0::BIT(24)) THEN + out_priv = 'A'; + RETURN out_priv; + END IF; + + -- The CALDAV:read-free-busy privilege MUST be aggregated in the DAV:read privilege. + -- 1 DAV:read + -- 512 CalDAV:read-free-busy + -- 4096 CALDAV:schedule-query-freebusy + IF (in_bits & 4609::BIT(24)) != 0::BIT(24) THEN + IF (in_bits & 1::BIT(24)) != 0::BIT(24) THEN + out_priv := 'R'; + ELSE + out_priv := 'F'; + END IF; + END IF; + + -- DAV:write => DAV:write MUST contain DAV:bind, DAV:unbind, DAV:write-properties and DAV:write-content + -- 2 DAV:write-properties + -- 4 DAV:write-content + -- 64 DAV:bind + -- 128 DAV:unbind + IF (in_bits & 198::BIT(24)) != 0::BIT(24) THEN + IF (in_bits & 6::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || 'W'; + ELSE + IF (in_bits & 64::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || 'B'; + END IF; + IF (in_bits & 128::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || 'U'; + END IF; + END IF; + END IF; + + RETURN out_priv; +END +$$ +LANGUAGE 'PlPgSQL' IMMUTABLE STRICT; + +CREATE or REPLACE FUNCTION get_permissions( INT, INT ) RETURNS TEXT AS $$ +DECLARE + in_from ALIAS FOR $1; + in_to ALIAS FOR $2; + out_confers TEXT; + bit_confers BIT(24); + group_role_no INT; + tmp_txt TEXT; + dbg TEXT DEFAULT ''; + r RECORD; + counter INT; +BEGIN + -- Self can always have full access + IF in_from = in_to THEN + RETURN 'A'; + END IF; + + -- dbg := 'S-'; + SELECT bits_to_legacy_privilege(r1.confers) INTO out_confers FROM relationship r1 + WHERE r1.from_user = in_from AND r1.to_user = in_to AND NOT usr_is_role(r1.to_user,'Group'); + IF FOUND THEN + RETURN dbg || out_confers; + END IF; + -- RAISE NOTICE 'No simple relationships between % and %', in_from, in_to; + + SELECT bit_or(r1.confers & r2.confers) INTO bit_confers + FROM relationship r1 + JOIN relationship r2 ON r1.to_user=r2.from_user + WHERE r1.from_user=in_from AND r2.to_user=in_to + AND r2.from_user IN (SELECT user_no FROM roles LEFT JOIN role_member USING(role_no) WHERE role_name='Group'); + IF bit_confers != 0::BIT(24) THEN + RETURN dbg || bits_to_legacy_privilege(bit_confers); + END IF; + + RETURN ''; + -- RAISE NOTICE 'No complex relationships between % and %', in_from, in_to; + + SELECT bits_to_legacy_privilege(r1.confers) INTO out_confers FROM relationship r1 LEFT OUTER JOIN relationship r2 ON(r1.to_user = r2.to_user) + WHERE r1.from_user = in_from AND r2.from_user = in_to AND r1.from_user != r2.from_user + AND NOT EXISTS( SELECT 1 FROM relationship r3 WHERE r3.from_user = r1.to_user ) ; + + IF FOUND THEN + -- dbg := 'H-'; + -- RAISE NOTICE 'Permissions to shared group % ', out_confers; + RETURN dbg || out_confers; + END IF; + + -- RAISE NOTICE 'No common group relationships between % and %', in_from, in_to; + + RETURN ''; +END; +$$ LANGUAGE 'plpgsql' IMMUTABLE STRICT; + + +CREATE or REPLACE FUNCTION get_group_role_no() RETURNS INT AS $$ + SELECT role_no FROM roles WHERE role_name = 'Group' +$$ LANGUAGE 'SQL' IMMUTABLE; + +CREATE or REPLACE FUNCTION has_legacy_privilege( INT, TEXT, INT ) RETURNS BOOLEAN AS $$ +DECLARE + in_from ALIAS FOR $1; + in_legacy_privilege ALIAS FOR $2; + in_to ALIAS FOR $3; + in_confers BIT(24); + group_role_no INT; +BEGIN + -- Self can always have full access + IF in_from = in_to THEN + RETURN TRUE; + END IF; + + SELECT get_group_role_no() INTO group_role_no; + SELECT legacy_privilege_to_bits(in_legacy_privilege) INTO in_confers; + + IF EXISTS(SELECT 1 FROM relationship WHERE from_user = in_from AND to_user = in_to + AND (in_confers & confers) = in_confers + AND NOT EXISTS(SELECT 1 FROM role_member WHERE to_user = user_no AND role_no = group_role_no) ) THEN + -- A direct relationship from A to B that grants sufficient + -- RAISE NOTICE 'Permissions directly granted'; + RETURN TRUE; + END IF; + + IF EXISTS( SELECT 1 FROM relationship r1 JOIN relationship r2 ON r1.to_user=r2.from_user + WHERE (in_confers & r1.confers & r2.confers) = in_confers + AND r1.from_user=in_from AND r2.to_user=in_to + AND r2.from_user IN (SELECT user_no FROM role_member WHERE role_no=group_role_no) ) THEN + -- An indirect relationship from A to B via group G that grants sufficient + -- RAISE NOTICE 'Permissions mediated via group'; + RETURN TRUE; + END IF; + + IF EXISTS( SELECT 1 FROM relationship r1 JOIN relationship r2 ON r1.to_user=r2.to_user + WHERE (in_confers & r1.confers & r2.confers) = in_confers + AND r1.from_user=in_from AND r2.from_user=in_to + AND r2.to_user IN (SELECT user_no FROM role_member WHERE role_no=group_role_no) + AND NOT EXISTS(SELECT 1 FROM relationship WHERE from_user=r2.to_user) ) THEN + -- An indirect reflexive relationship from both A & B to group G which grants sufficient + -- RAISE NOTICE 'Permissions to shared group'; + RETURN TRUE; + END IF; + + -- RAISE NOTICE 'No common group relationships between % and %', in_from, in_to; + + RETURN FALSE; +END; +$$ LANGUAGE 'plpgsql' IMMUTABLE STRICT; + + +-- Given a verbose DAV: or CalDAV: privilege name return the bitmask +CREATE or REPLACE FUNCTION privilege_to_bits( TEXT ) RETURNS BIT(24) AS $$ +DECLARE + raw_priv ALIAS FOR $1; + in_priv TEXT; +BEGIN + in_priv := trim(lower(regexp_replace(raw_priv, '^.*:', ''))); + IF in_priv = 'all' THEN + RETURN ~ 0::BIT(24); + END IF; + + RETURN (CASE + WHEN in_priv = 'read' THEN 4609 -- 1 + 512 + 4096 + WHEN in_priv = 'write' THEN 198 -- 2 + 4 + 64 + 128 + WHEN in_priv = 'write-properties' THEN 2 + WHEN in_priv = 'write-content' THEN 4 + WHEN in_priv = 'unlock' THEN 8 + WHEN in_priv = 'read-acl' THEN 16 + WHEN in_priv = 'read-current-user-privilege-set' THEN 32 + WHEN in_priv = 'bind' THEN 64 + WHEN in_priv = 'unbind' THEN 128 + WHEN in_priv = 'write-acl' THEN 256 + WHEN in_priv = 'read-free-busy' THEN 4608 -- 512 + 4096 + WHEN in_priv = 'schedule-deliver' THEN 7168 -- 1024 + 2048 + 4096 + WHEN in_priv = 'schedule-deliver-invite' THEN 1024 + WHEN in_priv = 'schedule-deliver-reply' THEN 2048 + WHEN in_priv = 'schedule-query-freebusy' THEN 4096 + WHEN in_priv = 'schedule-send' THEN 57344 -- 8192 + 16384 + 32768 + WHEN in_priv = 'schedule-send-invite' THEN 8192 + WHEN in_priv = 'schedule-send-reply' THEN 16384 + WHEN in_priv = 'schedule-send-freebusy' THEN 32768 + ELSE 0 END)::BIT(24); +END +$$ +LANGUAGE 'PlPgSQL' IMMUTABLE STRICT; + + +-- Given an array of verbose DAV: or CalDAV: privilege names return the bitmask +CREATE or REPLACE FUNCTION privilege_to_bits( TEXT[] ) RETURNS BIT(24) AS $$ +DECLARE + raw_privs ALIAS FOR $1; + in_priv TEXT; + out_bits BIT(24); + i INT; + all BIT(24); + start INT; + finish INT; +BEGIN + out_bits := 0::BIT(24); + all := ~ out_bits; + SELECT array_lower(raw_privs,1) INTO start; + SELECT array_upper(raw_privs,1) INTO finish; + FOR i IN start .. finish LOOP + SELECT out_bits | privilege_to_bits(raw_privs[i]) INTO out_bits; + IF out_bits = all THEN + RETURN all; + END IF; + END LOOP; + RETURN out_bits; +END +$$ +LANGUAGE 'PlPgSQL' IMMUTABLE STRICT; + + +-- This legacy conversion function will eventually be removed, once all logic +-- has been converted to use bitmaps, or to use the bits_to_priv() output. +-- +-- NOTE: Round-trip through this and then back through privilege_to_bits +-- function is lossy! Through privilege_to_bits() and back through +-- this one is not. +-- +CREATE or REPLACE FUNCTION bits_to_privilege( BIT(24) ) RETURNS TEXT[] AS $$ +DECLARE + in_bits ALIAS FOR $1; + out_priv TEXT[]; +BEGIN + out_priv := ARRAY[]::text[]; + IF in_bits = (~ 0::BIT(24)) THEN + out_priv := out_priv || ARRAY['DAV:all']; + END IF; + + IF (in_bits & 513::BIT(24)) != 0::BIT(24) THEN + IF (in_bits & 1::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['DAV:read']; + END IF; + IF (in_bits & 512::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['caldav:read-free-busy']; + END IF; + END IF; + + IF (in_bits & 198::BIT(24)) != 0::BIT(24) THEN + IF (in_bits & 198::BIT(24)) = 198::BIT(24) THEN + out_priv := out_priv || ARRAY['DAV:write']; + ELSE + IF (in_bits & 2::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['DAV:write-properties']; + END IF; + IF (in_bits & 4::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['DAV:write-content']; + END IF; + IF (in_bits & 64::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['DAV:bind']; + END IF; + IF (in_bits & 128::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['DAV:unbind']; + END IF; + END IF; + END IF; + + IF (in_bits & 8::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['DAV:unlock']; + END IF; + + IF (in_bits & 16::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['DAV:read-acl']; + END IF; + + IF (in_bits & 32::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['DAV:read-current-user-privilege-set']; + END IF; + + IF (in_bits & 256::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['DAV:write-acl']; + END IF; + + IF (in_bits & 7168::BIT(24)) != 0::BIT(24) THEN + IF (in_bits & 7168::BIT(24)) = 7168::BIT(24) THEN + out_priv := out_priv || ARRAY['caldav:schedule-deliver']; + ELSE + IF (in_bits & 1024::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['caldav:schedule-deliver-invite']; + END IF; + IF (in_bits & 2048::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['caldav:schedule-deliver-reply']; + END IF; + IF (in_bits & 4096::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['caldav:schedule-query-freebusy']; + END IF; + END IF; + END IF; + + IF (in_bits & 57344::BIT(24)) != 0::BIT(24) THEN + IF (in_bits & 57344::BIT(24)) = 57344::BIT(24) THEN + out_priv := out_priv || ARRAY['caldav:schedule-send']; + ELSE + IF (in_bits & 8192::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['caldav:schedule-send-invite']; + END IF; + IF (in_bits & 16384::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['caldav:schedule-send-reply']; + END IF; + IF (in_bits & 32768::BIT(24)) != 0::BIT(24) THEN + out_priv := out_priv || ARRAY['caldav:schedule-send-freebusy']; + END IF; + END IF; + END IF; + + RETURN out_priv; +END +$$ +LANGUAGE 'PlPgSQL' IMMUTABLE STRICT; + + + + + + + + + + + BEGIN; SELECT check_db_revision(1,2,5); -\i dba/better_perms.sql -- DAV Privileges implementation -- diff --git a/dba/patches/1.2.7.sql b/dba/patches/1.2.7.sql index 0143b9da..b7583137 100644 --- a/dba/patches/1.2.7.sql +++ b/dba/patches/1.2.7.sql @@ -14,8 +14,10 @@ CREATE TABLE sync_tokens ( CREATE TABLE sync_changes ( sync_time TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp, - status INT2, - dav_id INT8 REFERENCES calendar_item(dav_id) + collection_id INT8 REFERENCES collection(collection_id) ON DELETE CASCADE ON UPDATE CASCADE, + sync_status INT, + dav_id INT8 REFERENCES calendar_item(dav_id) ON DELETE SET NULL ON UPDATE RESTRICT, + dav_name TEXT ); SELECT new_db_revision(1,2,7, 'Juli' ); @@ -38,8 +40,8 @@ BEGIN RETURN FALSE; END IF; SELECT dav_id INTO tmp_int FROM calendar_item WHERE dav_name = in_dav_name; - INSERT INTO sync_changes ( collection_id, status, dav_id) - VALUES( in_collection_id, in_status::INT2, tmp_int); + INSERT INTO sync_changes ( collection_id, sync_status, dav_id, dav_name) + VALUES( in_collection_id, in_status, tmp_int, in_dav_name); RETURN TRUE; END $$ LANGUAGE 'PlPgSQL' VOLATILE STRICT; @@ -51,14 +53,16 @@ DECLARE in_collection_id ALIAS FOR $2; tmp_int INT8; BEGIN - SELECT 1 INTO tmp_int FROM sync_changes - WHERE collection_id = in_collection_id - AND sync_time > (SELECT modification_time FROM sync_tokens WHERE sync_token = in_old_sync_token) - LIMIT 1; - IF NOT FOUND THEN - RETURN in_old_sync_token; + IF in_old_sync_token > 0 THEN + SELECT 1 INTO tmp_int FROM sync_changes + WHERE collection_id = in_collection_id + AND sync_time > (SELECT modification_time FROM sync_tokens WHERE sync_token = in_old_sync_token) + LIMIT 1; + IF NOT FOUND THEN + RETURN in_old_sync_token; + END IF; END IF; - SELECT nextval('dav_id_seq') INTO tmp_int; + SELECT nextval('sync_tokens_sync_token_seq') INTO tmp_int; INSERT INTO sync_tokens(collection_id, sync_token) VALUES( in_collection_id, tmp_int ); RETURN tmp_int; END diff --git a/inc/CalDAVRequest.php b/inc/CalDAVRequest.php index e2b09ddd..36ade877 100644 --- a/inc/CalDAVRequest.php +++ b/inc/CalDAVRequest.php @@ -823,6 +823,14 @@ EOSQL; } + /** + * Returns the ID of the collection of, or containing this request + */ + function CollectionId( ) { + return $this->collection_id; + } + + /** * Returns the array of supported privileges converted into XMLElements */ diff --git a/inc/DAVResource.php b/inc/DAVResource.php index 4960c1c0..a8d7d782 100644 --- a/inc/DAVResource.php +++ b/inc/DAVResource.php @@ -81,41 +81,16 @@ class DAVResource global $c; foreach( $row AS $k => $v ) { + dbg_error_log( 'resource', 'Processing resource property "%s" has "%s".', $row->dav_name, $k ); switch ( $k ) { - case 'dav_name': - $this->href = ConstructURL( '/'.$this->dav_name.'/', true ); - $this->{$k} = $v; - break; - case 'dav_etag': - $this->unique_tag = $this->dav_etag; - break; - - case 'caldav_data': - $this->content = $this->caldav_data; - break; - - case 'updated': - $this->modified = ISODateToHTTPDate($this->updated); - break; - - case 'created': - $this->modified = ISODateToHTTPDate($this->joined); - break; - - case 'username': - $this->owner = ConstructURL( '/'.$this->username.'/', true );; - $this->principal_url = ConstructURL( '/'.$this->username.'/', true );; + $this->unique_tag = '"'.$v.'"'; break; default: $this->{$k} = $v; } } - - if ( ! isset($this->content) ) { - $this->contenttype = 'http/unix-directory'; - } } @@ -127,9 +102,13 @@ class DAVResource if ( $reply === null ) $reply = $GLOBALS['reply']; - dbg_error_log( 'caldav', 'Processing "%s" on "%s".', $tag, $this->path ); + dbg_error_log( 'resource', 'Processing "%s" on "%s".', $tag, $this->dav_name ); switch( $tag ) { + case 'DAV::href': + $prop->NewElement('href', ConstructURL($this->dav_name) ); + break; + case 'DAV::getcontenttype': $prop->NewElement('getcontenttype', $this->contenttype ); break; @@ -142,13 +121,13 @@ class DAVResource $prop->NewElement('displayname', $this->displayname ); break; - case 'DAV::getlastmodified': - $prop->NewElement('getlastmodified', $this->modified ); - break; +// case 'DAV::getlastmodified': +// $prop->NewElement('getlastmodified', $this->modified ); +// break; - case 'DAV::creationdate': - $prop->NewElement('creationdate', $this->created ); - break; +// case 'DAV::creationdate': +// $prop->NewElement('creationdate', $this->created ); +// break; case 'DAV::getcontentlanguage': $locale = (isset($c->current_locale) ? $c->current_locale : ''); @@ -156,10 +135,10 @@ class DAVResource $prop->NewElement('getcontentlanguage', $locale ); break; - case 'DAV::owner': - // After a careful reading of RFC3744 we see that this must be the principal-URL of the owner - $reply->DAVElement( $prop, 'owner', $reply->href( $this->principal_url ) ); - break; +// case 'DAV::owner': +// // After a careful reading of RFC3744 we see that this must be the principal-URL of the owner +// $reply->DAVElement( $prop, 'owner', $reply->href( $this->principal_url) ) ); +// break; // Empty tag responses. case 'DAV::alternate-URI-set': @@ -172,7 +151,7 @@ class DAVResource $not_found[] = $reply->Tag($tag); } else { - $prop->NewElement('getetag', $this->etag ); + $prop->NewElement('getetag', $this->unique_tag ); } break; @@ -189,23 +168,61 @@ class DAVResource } break; - case 'DAV::sync-token': - if ( $this->_is_collection ) { - $prop->NewElement('sync-token', $this->NewSyncToken() ); - } - else { - $not_found[] = $reply->Tag($tag); - } - break; - default: - dbg_error_log( 'caldav', 'Request for unsupported property "%s" of path "%s".', $tag, $this->href ); + dbg_error_log( 'resource', 'Request for unsupported property "%s" of path "%s".', $tag, $this->href ); return false; } return true; } + /** + * Construct XML propstat fragment for this resource + * + * @param array $properties The requested properties for this resource + * + * @return string An XML fragment with the requested properties for this resource + */ + function GetPropStat( $properties ) { + global $session, $c, $request, $reply; + + dbg_error_log('resource',': GetPropStat: href "%s"', $this->dav_name ); + + $prop = new XMLElement('prop'); + $denied = array(); + $not_found = array(); + foreach( $properties AS $k => $tag ) { + dbg_error_log( 'resource', 'Looking at resource "%s" for property [%s]"%s".', $this->href, $k, $tag ); + if ( ! $this->ResourceProperty($tag, $prop, $reply) ) { + dbg_error_log( 'resource', 'Request for unsupported property "%s" of resource "%s".', $tag, $this->href ); + $not_found[] = $reply->Tag($tag); + } + } + $status = new XMLElement('status', 'HTTP/1.1 200 OK' ); + + $elements = array( new XMLElement( 'propstat', array($prop,$status) ) ); + + if ( count($denied) > 0 ) { + $status = new XMLElement('status', 'HTTP/1.1 403 Forbidden' ); + $noprop = new XMLElement('prop'); + foreach( $denied AS $k => $v ) { + $noprop->NewElement( $v ); + } + $elements[] = new XMLElement( 'propstat', array( $noprop, $status) ); + } + + if ( count($not_found) > 0 ) { + $status = new XMLElement('status', 'HTTP/1.1 404 Not Found' ); + $noprop = new XMLElement('prop'); + foreach( $not_found AS $k => $v ) { + $noprop->NewElement( $v ); + } + $elements[] = new XMLElement( 'propstat', array( $noprop, $status) ); + } + return $elements; + } + + /** * Render XML for this resource * @@ -228,14 +245,6 @@ class DAVResource dbg_error_log( 'principal', 'Request for unsupported property "%s" of principal "%s".', $tag, $this->username ); $not_found[] = $reply->Tag($tag); } - - default: - if ( ! $request->ServerProperty( $tag, $prop, $reply ) ) { - dbg_error_log( 'principal', 'Request for unsupported property "%s" of principal "%s".', $tag, $this->username ); - $not_found[] = $reply->Tag($tag); - } - break; - } } if ( $props_only ) return $prop; diff --git a/inc/caldav-DELETE.php b/inc/caldav-DELETE.php index d21afbc5..4072b546 100644 --- a/inc/caldav-DELETE.php +++ b/inc/caldav-DELETE.php @@ -66,15 +66,22 @@ else { /** * We read the resource first, so we can check if it matches (or does not match) */ - $qry = new PgQuery( "SELECT cd.dav_etag, ci.uid FROM caldav_data cd JOIN calendar_item ci USING (dav_id) WHERE cd.user_no = ? AND cd.dav_name = ?;", $request->user_no, $request->path ); + $escaped_path = qpg($request->path); + $qry = new PgQuery( "SELECT cd.dav_etag, ci.uid, cd.collection_id FROM caldav_data cd JOIN calendar_item ci USING (dav_id) WHERE cd.user_no = ? AND cd.dav_name = $escaped_path;", $request->user_no ); if ( $qry->Exec("DELETE") && $qry->rows == 1 ) { $delete_row = $qry->Fetch(); if ( (isset($request->etag_if_match) && $request->etag_if_match != $delete_row->dav_etag) ) { $request->DoResponse( 412, translate("Resource has changed on server - not deleted") ); } - $qry = new PgQuery( "DELETE FROM caldav_data WHERE user_no = ? AND dav_name = ?;", $request->user_no, $request->path ); + + $collection_id = $delete_row->collection_id; + $sql = <<Exec("DELETE") ) { - $qry = new PgQuery( "DELETE FROM property WHERE dav_name = ?;", $request->path ); $qry->Exec("DELETE"); /** @todo we should write a trigger to delete property records when caldav_data or collection is deleted */ + $qry = new PgQuery( "DELETE FROM property WHERE dav_name = $escaped_path;" ); $qry->Exec("DELETE"); /** @todo we should write a trigger to delete property records when caldav_data or collection is deleted */ @dbg_error_log( "DELETE", "DELETE: User: %d, ETag: %s, Path: %s", $session->user_no, $request->etag_if_match, $request->path); if ( function_exists('log_caldav_action') ) { log_caldav_action( 'DELETE', $delete_row->uid, $request->user_no, $request->collection_id, $request->path ); diff --git a/inc/caldav-REPORT-sync-collection.php b/inc/caldav-REPORT-sync-collection.php index e226d68c..2e641867 100644 --- a/inc/caldav-REPORT-sync-collection.php +++ b/inc/caldav-REPORT-sync-collection.php @@ -1,5 +1,5 @@ GetPath('/DAV::sync-collection/DAV::prop'); + +$props = $xmltree->GetElements('DAV::prop'); +$v = $props[0]; +$props = $v->GetContent(); $proplist = array(); foreach( $props AS $k => $v ) { - $proplist[] = $v->GetContent(); + $proplist[] = $v->GetTag(); } + function display_status( $status_code ) { - return sprintf( 'HTTP/1.1 %03d %s', $status_code, getStatusMessage($status_code) ); + return sprintf( 'HTTP/1.1 %03d %s', intval($status_code), getStatusMessage($status_code) ); } $sql = "SELECT new_sync_token(?,?)"; @@ -25,22 +29,42 @@ $qry = new PgQuery($sql, $sync_token, $request->CollectionId()); if ( !$qry->Exec("REPORT",__LINE__,__FILE__) || $qry->rows <= 0 ) { $request->DoResponse( 500, translate("Database error") ); } +$row = $qry->Fetch(); +$new_token = $row->new_sync_token; -$sql = "SELECT * FROM collection LEFT JOIN sync_changes USING(collection_id) sync_changes LEFT JOIN calendar_item USING (dav_id) LEFT JOIN caldav_data USING (dav_id) WHERE collection_id = ? AND sync_time > (SELECT modification_time FROM sync_tokens WHERE sync_token = ?)"; -$qry = new PgQuery($sql, $request->CollectionId(), $sync_token); +if ( $sync_token == 0 ) { + $sql = <<CollectionId()); +} +else { + $sql = << (SELECT modification_time FROM sync_tokens WHERE sync_token = ?) +EOSQL; + $qry = new PgQuery($sql, $request->CollectionId(), $sync_token); +} -if ( $qry->Exec("REPORT",__LINE__,__FILE__) && $qry->rows > 0 ) { +if ( $qry->Exec("REPORT",__LINE__,__FILE__) ) { while( $object = $qry->Fetch() ) { $resultset = array( - new XMLElement( 'href', $object->href ), - new XMLElement( 'status', display_status($object->status) ) + new XMLElement( 'href', ConstructURL($object->dav_name) ), + new XMLElement( 'status', display_status($object->sync_status) ) ); if ( $status != 404 ) { - $dav_object = new DavObject($object); - $resultset[] = $dav_object->GetPropStat( $proplist ); + $dav_resource = new DAVResource($object); + $resultset = array_merge( $resultset, $dav_resource->GetPropStat($proplist) ); } $responses[] = new XMLElement( 'sync-response', $resultset ); } + $responses[] = new XMLElement( 'sync-token', $new_token ); } else { $request->DoResponse( 500, translate("Database error") ); diff --git a/testing/tests/regression-suite/099-REPORT-sync-initial.result b/testing/tests/regression-suite/099-REPORT-sync-initial.result new file mode 100644 index 00000000..efdae12f --- /dev/null +++ b/testing/tests/regression-suite/099-REPORT-sync-initial.result @@ -0,0 +1,21 @@ +HTTP/1.1 207 Multi-Status +Date: Dow, 01 Jan 2000 00:00:00 GMT +DAV: 1, 2, 3, access-control, calendar-access, calendar-schedule +ETag: "d668266633188d6d2690a2eb491bba0c" +Content-Length: 404 +Content-Type: text/xml; charset="utf-8" + + + + + /caldav.php/user1/home/3F4CF6227300FD062D9EF3CDFB30D32D-0.ics + HTTP/1.1 201 Created + + + "2c32a2f8aba853654eb17fe037a4db4d" + + HTTP/1.1 200 OK + + + 1 + diff --git a/testing/tests/regression-suite/099-REPORT-sync-initial.test b/testing/tests/regression-suite/099-REPORT-sync-initial.test new file mode 100644 index 00000000..e161d7bd --- /dev/null +++ b/testing/tests/regression-suite/099-REPORT-sync-initial.test @@ -0,0 +1,19 @@ +# +# Check for support of REPORT sync-collection with no sync-token +# +TYPE=REPORT +URL=http://mycaldav/caldav.php/user1/home/ +HEADER=User-agent: sync-collection initial REPORT +HEADER=Content-type: text/xml +HEAD + +BEGINDATA + + + + + + + +ENDDATA + diff --git a/testing/tests/regression-suite/598-REPORT-sync-initial.result b/testing/tests/regression-suite/598-REPORT-sync-initial.result new file mode 100644 index 00000000..704c1584 --- /dev/null +++ b/testing/tests/regression-suite/598-REPORT-sync-initial.result @@ -0,0 +1,161 @@ +HTTP/1.1 207 Multi-Status +Date: Dow, 01 Jan 2000 00:00:00 GMT +DAV: 1, 2, 3, access-control, calendar-access, calendar-schedule +ETag: "7ab7cf2174a67bf4fc26271956be7d90" +Content-Length: 4528 +Content-Type: text/xml; charset="utf-8" + + + + + /caldav.php/user1/home/AAA9318E-37D9-4319-8626-95ECD3D3B243.ics + HTTP/1.1 201 Created + + + "5f050eca5480bbebbe9428222570913d" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/da81c0ee-7871-11db-c6d6-f6927c144649.ics + HTTP/1.1 201 Created + + + "421abf7e4848d2fecbf64217ed205d4b" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/71e2ae82-7870-11db-c6d6-f6927c144649.ics + HTTP/1.1 201 Created + + + "0d7a68984bf525342d22b8924a57e8e2" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/e6eb5bc9-f7f9-4a0a-94e8-8e90eefc7d08.ics + HTTP/1.1 201 Created + + + "8f581a053df6d833254756dfd7553d37" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/e70576e9-c1e0-431e-a507-0386fd82f223.ics + HTTP/1.1 201 Created + + + "e8060931f30c1798ac58ffbe4ec0bffc" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/b1679f77-673d-4f46-b3eb-2420e1bba301.ics + HTTP/1.1 201 Created + + + "a2990674708634a311bb98a59865ca50" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/0575d895-a006-4ed8-9be6-0d1b6b6b1f96.ics + HTTP/1.1 201 Created + + + "00ad5eb1eb5507884710b0b66aa5d5c4" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/917b9e47-b748-4550-a566-657fbe672447.ics + HTTP/1.1 201 Created + + + "cb3d9dc3e8c157f53eba3ea0e1e0f146" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/2178279a-aec2-471f-832d-1f6df6203f2f.ics + HTTP/1.1 201 Created + + + "509b0f0d8a3363379f9f5727f5dd74a0" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/fbd57454-d966-4a14-8341-abe1edb1ae66.ics + HTTP/1.1 201 Created + + + "ac90acd649c25070b1a2a17fb31a105a" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/1906b3ca-4890-468a-9b58-1de74bf2c716.ics + HTTP/1.1 201 Created + + + "5def8ae2b20893a1c7f4dbaeb008f2f1" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/9d050be7-8a02-4355-8ed3-02a9fc5f473f.ics + HTTP/1.1 201 Created + + + "08a435c2abaf38f4a50a997343c098a7" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/4aaf8f37-f232-4c8e-a72e-e171d4c4fe54.ics + HTTP/1.1 201 Created + + + "a1c6404d61190f9574e2bfd69383f144" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/20061101T073004Z.ics + HTTP/1.1 201 Created + + + "c3658901fd4689d4a1e1d6f08601ef4f" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/3F4CF6227300FD062D9EF3CDFB30D32D-0.ics + HTTP/1.1 201 Created + + + "2c32a2f8aba853654eb17fe037a4db4d" + + HTTP/1.1 200 OK + + + 2 + diff --git a/testing/tests/regression-suite/598-REPORT-sync-initial.test b/testing/tests/regression-suite/598-REPORT-sync-initial.test new file mode 100644 index 00000000..e161d7bd --- /dev/null +++ b/testing/tests/regression-suite/598-REPORT-sync-initial.test @@ -0,0 +1,19 @@ +# +# Check for support of REPORT sync-collection with no sync-token +# +TYPE=REPORT +URL=http://mycaldav/caldav.php/user1/home/ +HEADER=User-agent: sync-collection initial REPORT +HEADER=Content-type: text/xml +HEAD + +BEGINDATA + + + + + + + +ENDDATA + diff --git a/testing/tests/regression-suite/599-REPORT-sync-changed.result b/testing/tests/regression-suite/599-REPORT-sync-changed.result new file mode 100644 index 00000000..24c435c5 --- /dev/null +++ b/testing/tests/regression-suite/599-REPORT-sync-changed.result @@ -0,0 +1,161 @@ +HTTP/1.1 207 Multi-Status +Date: Dow, 01 Jan 2000 00:00:00 GMT +DAV: 1, 2, 3, access-control, calendar-access, calendar-schedule +ETag: "1e80e92dfce8e43ed452a56e350b577b" +Content-Length: 4525 +Content-Type: text/xml; charset="utf-8" + + + + + /caldav.php/user1/home/20061101T073004Z.ics + HTTP/1.1 201 Created + + + "c3658901fd4689d4a1e1d6f08601ef4f" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/4aaf8f37-f232-4c8e-a72e-e171d4c4fe54.ics + HTTP/1.1 201 Created + + + "a1c6404d61190f9574e2bfd69383f144" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/9d050be7-8a02-4355-8ed3-02a9fc5f473f.ics + HTTP/1.1 201 Created + + + "08a435c2abaf38f4a50a997343c098a7" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/1906b3ca-4890-468a-9b58-1de74bf2c716.ics + HTTP/1.1 201 Created + + + "5def8ae2b20893a1c7f4dbaeb008f2f1" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/fbd57454-d966-4a14-8341-abe1edb1ae66.ics + HTTP/1.1 201 Created + + + "ac90acd649c25070b1a2a17fb31a105a" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/2178279a-aec2-471f-832d-1f6df6203f2f.ics + HTTP/1.1 201 Created + + + "509b0f0d8a3363379f9f5727f5dd74a0" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/917b9e47-b748-4550-a566-657fbe672447.ics + HTTP/1.1 201 Created + + + "cb3d9dc3e8c157f53eba3ea0e1e0f146" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/0575d895-a006-4ed8-9be6-0d1b6b6b1f96.ics + HTTP/1.1 201 Created + + + "00ad5eb1eb5507884710b0b66aa5d5c4" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/b1679f77-673d-4f46-b3eb-2420e1bba301.ics + HTTP/1.1 201 Created + + + "a2990674708634a311bb98a59865ca50" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/e70576e9-c1e0-431e-a507-0386fd82f223.ics + HTTP/1.1 201 Created + + + "e8060931f30c1798ac58ffbe4ec0bffc" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/e6eb5bc9-f7f9-4a0a-94e8-8e90eefc7d08.ics + HTTP/1.1 201 Created + + + "8f581a053df6d833254756dfd7553d37" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/71e2ae82-7870-11db-c6d6-f6927c144649.ics + HTTP/1.1 201 Created + + + "0d7a68984bf525342d22b8924a57e8e2" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/da81c0ee-7871-11db-c6d6-f6927c144649.ics + HTTP/1.1 201 Created + + + "421abf7e4848d2fecbf64217ed205d4b" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/AAA9318E-37D9-4319-8626-95ECD3D3B243.ics + HTTP/1.1 201 Created + + + "5f050eca5480bbebbe9428222570913d" + + HTTP/1.1 200 OK + + + + /caldav.php/user1/home/AAA9318E-37D9-4319-8626-95ECD3D3B243.ics + HTTP/1.1 200 OK + + + "5f050eca5480bbebbe9428222570913d" + + HTTP/1.1 200 OK + + + 3 + diff --git a/testing/tests/regression-suite/599-REPORT-sync-changed.test b/testing/tests/regression-suite/599-REPORT-sync-changed.test new file mode 100644 index 00000000..9529d479 --- /dev/null +++ b/testing/tests/regression-suite/599-REPORT-sync-changed.test @@ -0,0 +1,19 @@ +# +# Check for support of REPORT sync-collection with no sync-token +# +TYPE=REPORT +URL=http://mycaldav/caldav.php/user1/home/ +HEADER=User-agent: sync-collection changes REPORT +HEADER=Content-type: text/xml +HEAD + +BEGINDATA + + + 1 + + + + +ENDDATA +