From 3df6ccc4baf8b51a40b63627e3a6d8f0e6fc8451 Mon Sep 17 00:00:00 2001 From: Andrew McMillan Date: Wed, 4 Nov 2009 00:17:10 +1300 Subject: [PATCH] Getting 'MOVE' working has proven surprisingly complex. --- dba/appuser_permissions.txt | 1 + dba/caldav_functions.sql | 45 +++++- dba/davical.sql | 32 ++-- htdocs/caldav.php | 1 + inc/AwlQuery.php | 8 + inc/CalDAVPrincipal.php | 16 +- inc/CalDAVRequest.php | 6 + inc/DAVResource.php | 285 +++++++++++++++++++++++++++-------- inc/HTTPAuthSession.php | 10 +- inc/always.php | 15 +- inc/always.php.in | 15 +- inc/caldav-MOVE.php | 125 ++++++++++++--- inc/caldav-OPTIONS.php | 12 +- inc/caldav-PUT-functions.php | 29 ++-- 14 files changed, 466 insertions(+), 134 deletions(-) diff --git a/dba/appuser_permissions.txt b/dba/appuser_permissions.txt index 325af656..ed3da51a 100644 --- a/dba/appuser_permissions.txt +++ b/dba/appuser_permissions.txt @@ -35,6 +35,7 @@ GRANT SELECT,INSERT,UPDATE,DELETE ON relationship_type ON sync_tokens ON sync_changes + ON grants GRANT SELECT,UPDATE ON relationship_type_rt_id_seq diff --git a/dba/caldav_functions.sql b/dba/caldav_functions.sql index 45333372..daf92d27 100644 --- a/dba/caldav_functions.sql +++ b/dba/caldav_functions.sql @@ -417,7 +417,8 @@ BEGIN IF TG_OP = 'UPDATE' THEN IF NEW.dav_name != OLD.dav_name THEN UPDATE caldav_data - SET dav_name = replace( dav_name, OLD.dav_name, NEW.dav_name) + SET dav_name = replace( dav_name, OLD.dav_name, NEW.dav_name), + user_no = NEW.user_no WHERE substring(dav_name from 1 for char_length(OLD.dav_name)) = OLD.dav_name; END IF; END IF; @@ -475,6 +476,44 @@ CREATE TRIGGER caldav_data_modified AFTER INSERT OR UPDATE OR DELETE ON caldav_d FOR EACH ROW EXECUTE PROCEDURE caldav_data_modified(); +DROP TRIGGER caldav_data_sync_dav_id ON caldav_data CASCADE; +DROP TRIGGER calendar_item_sync_dav_id ON calendar_item CASCADE; +CREATE or REPLACE FUNCTION sync_dav_id ( ) RETURNS TRIGGER AS $$ + DECLARE + BEGIN + + IF TG_OP = 'DELETE' THEN + -- Just let the ON DELETE CASCADE handle this case + RETURN OLD; + END IF; + + IF NEW.dav_id IS NULL THEN + NEW.dav_id = nextval('dav_id_seq'); + END IF; + + IF TG_OP = 'UPDATE' THEN + IF OLD.dav_id != NEW.dav_id OR OLD.collection_id != NEW.collection_id + OR OLD.user_no != NEW.user_no OR OLD.dav_name != NEW.dav_name THEN + UPDATE calendar_item SET dav_id = NEW.dav_id, user_no = NEW.user_no, + collection_id = NEW.collection_id, dav_name = NEW.dav_name + WHERE dav_name = OLD.dav_name OR dav_id = OLD.dav_id; + END IF; + RETURN NEW; + END IF; + + UPDATE calendar_item SET dav_id = NEW.dav_id, user_no = NEW.user_no, + collection_id = NEW.collection_id, dav_name = NEW.dav_name + WHERE dav_name = NEW.dav_name OR dav_id = NEW.dav_id; + + RETURN NEW; + + END +$$ LANGUAGE 'plpgsql'; +CREATE TRIGGER caldav_data_sync_dav_id AFTER INSERT OR UPDATE ON caldav_data + FOR EACH ROW EXECUTE PROCEDURE sync_dav_id(); + + + -- New in 1.2.6 CREATE or REPLACE FUNCTION legacy_privilege_to_bits( TEXT ) RETURNS BIT(24) AS $$ @@ -927,14 +966,14 @@ BEGIN -- We need to canonicalise the path, so: -- If it matches '/' + some characters (+ optional '/') => a principal URL IF in_path ~ '^/[^/]+/?$' THEN - alt1_path := replace(in_path, '/', '') + alt1_path := replace(in_path, '/', ''); SELECT principal_privileges(in_accessor,principal_id) INTO out_conferred FROM usr JOIN principal USING(user_no) WHERE username = alt1_path; RETURN out_conferred; END IF; -- Otherwise look for the longest segment matching up to the last '/', or if we append one, or if we replace a final '.ics' with one. alt1_path := in_path; - IF alt1_path ~ '\.ics$' THEN + IF alt1_path ~ E'\\.ics$' THEN alt1_path := substr(alt1_path, 1, length(alt1_path) - 4) || '/'; END IF; alt2_path := regexp_replace( in_path, '[^/]*$', ''); diff --git a/dba/davical.sql b/dba/davical.sql index e5173184..a5ffffc2 100644 --- a/dba/davical.sql +++ b/dba/davical.sql @@ -209,43 +209,41 @@ CREATE TABLE freebusy_ticket ( ); -CREATE or REPLACE FUNCTION sync_dav_id ( ) RETURNS TRIGGER AS ' + +CREATE or REPLACE FUNCTION sync_dav_id ( ) RETURNS TRIGGER AS $$ DECLARE BEGIN - IF TG_OP = ''DELETE'' THEN + IF TG_OP = 'DELETE' THEN -- Just let the ON DELETE CASCADE handle this case RETURN OLD; END IF; IF NEW.dav_id IS NULL THEN - NEW.dav_id = nextval(''dav_id_seq''); + NEW.dav_id = nextval('dav_id_seq'); END IF; - IF TG_OP = ''UPDATE'' THEN - IF OLD.dav_id = NEW.dav_id THEN - -- Nothing to do - RETURN NEW; + IF TG_OP = 'UPDATE' THEN + IF OLD.dav_id != NEW.dav_id OR OLD.collection_id != NEW.collection_id + OR OLD.user_no != NEW.user_no OR OLD.dav_name != NEW.dav_name THEN + UPDATE calendar_item SET dav_id = NEW.dav_id, user_no = NEW.user_no, + collection_id = NEW.collection_id, dav_name = NEW.dav_name + WHERE dav_name = OLD.dav_name OR dav_id = OLD.dav_id; END IF; + RETURN NEW; END IF; - IF TG_RELNAME = ''caldav_data'' THEN - UPDATE calendar_item SET dav_id = NEW.dav_id WHERE user_no = NEW.user_no AND dav_name = NEW.dav_name; - ELSE - UPDATE caldav_data SET dav_id = NEW.dav_id WHERE user_no = NEW.user_no AND dav_name = NEW.dav_name; - END IF; + UPDATE calendar_item SET dav_id = NEW.dav_id, user_no = NEW.user_no, + collection_id = NEW.collection_id, dav_name = NEW.dav_name + WHERE dav_name = NEW.dav_name OR dav_id = NEW.dav_id; RETURN NEW; END -' LANGUAGE 'plpgsql'; - +$$ LANGUAGE 'plpgsql'; CREATE TRIGGER caldav_data_sync_dav_id AFTER INSERT OR UPDATE ON caldav_data FOR EACH ROW EXECUTE PROCEDURE sync_dav_id(); -CREATE TRIGGER calendar_item_sync_dav_id AFTER INSERT OR UPDATE ON calendar_item - FOR EACH ROW EXECUTE PROCEDURE sync_dav_id(); - -- Only needs SELECT access by website. CREATE TABLE principal_type ( diff --git a/htdocs/caldav.php b/htdocs/caldav.php index cf5fc13c..1da98261 100644 --- a/htdocs/caldav.php +++ b/htdocs/caldav.php @@ -53,6 +53,7 @@ switch ( $request->method ) { case 'MKCOL': include_once("caldav-MKCOL.php"); break; case 'PUT': include_once("caldav-PUT.php"); break; case 'POST': include_once("caldav-POST.php"); break; + case 'MOVE': include_once("caldav-MOVE.php"); break; case 'GET': include_once("caldav-GET.php"); break; case 'HEAD': include_once("caldav-GET.php"); break; case 'DELETE': include_once("caldav-DELETE.php"); break; diff --git a/inc/AwlQuery.php b/inc/AwlQuery.php index 7b3586c6..3bd1ef49 100644 --- a/inc/AwlQuery.php +++ b/inc/AwlQuery.php @@ -350,6 +350,14 @@ class AwlQuery } + /** + * Return the count of rows retrieved/affected + */ + function rows() { + return $this->rows; + } + + /** * Execute the query, logging any debugging. * diff --git a/inc/CalDAVPrincipal.php b/inc/CalDAVPrincipal.php index a9640b5c..d86fc460 100644 --- a/inc/CalDAVPrincipal.php +++ b/inc/CalDAVPrincipal.php @@ -163,6 +163,8 @@ class CalDAVPrincipal if ( !isset($this->modified) ) $this->modified = ISODateToHTTPDate($this->updated); if ( !isset($this->created) ) $this->created = ISODateToHTTPDate($this->joined); + $this->dav_etag = md5($this->username . $this->updated); + $this->by_email = false; $this->principal_url = ConstructURL( '/'.$this->username.'/', true ); $this->url = $this->principal_url; @@ -180,9 +182,9 @@ class CalDAVPrincipal $this->dropbox_url = sprintf( '%s.drop/', $this->url); $this->notifications_url = sprintf( '%s.notify/', $this->url); - if ( isset ( $c->notifications_server ) ) { + if ( isset ( $c->notifications_server ) ) { $this->xmpp_uri = 'xmpp:pubsub.'.$c->notifications_server['host'].'?pubsub;node=/home/'.$c->notifications_server['host']; - $this->xmpp_uri .= '/'.preg_replace ( '/@.*$/', '', $c->notifications_server['jid'] ).'/DAViCal'.$this->url; + $this->xmpp_uri .= '/'.preg_replace ( '/@.*$/', '', $c->notifications_server['jid'] ).'/DAViCal'.$this->url; $this->xmpp_server = $c->notifications_server['host']; } @@ -284,7 +286,7 @@ class CalDAVPrincipal $username = $user->username; } } - elseif( $user = getUserByName( $username, 'caldav') ) { + elseif( $user = getUserByName( $username, 'principal') ) { $user_no = $user->user_no; } return $username; @@ -315,6 +317,14 @@ class CalDAVPrincipal } + /** + * Return the privileges bits for the current session user to this resource + */ + function Privileges() { + return $this->privileges; + } + + /** * Returns a representation of the principal as a collection */ diff --git a/inc/CalDAVRequest.php b/inc/CalDAVRequest.php index 6951dd6e..b932cb7d 100644 --- a/inc/CalDAVRequest.php +++ b/inc/CalDAVRequest.php @@ -77,6 +77,12 @@ class CalDAVRequest */ var $collection_type; + /** + * The type of collection being requested: + * calendar, schedule-inbox, schedule-outbox + */ + protected $exists; + /** * A static structure of supported privileges. */ diff --git a/inc/DAVResource.php b/inc/DAVResource.php index 9ad0960f..73feaed6 100644 --- a/inc/DAVResource.php +++ b/inc/DAVResource.php @@ -26,25 +26,25 @@ function privilege_to_bits( $raw_privs ) { foreach( $raw_privs AS $priv ) { $trim_priv = trim(strtolower(preg_replace( '/^.*:/', '', $priv))); switch( $trim_priv ) { - case 'read' : $out_priv &= 4609; break; // 1 + 512 + 4096 - case 'write' : $out_priv &= 198; break; // 2 + 4 + 64 + 128 - case 'write-properties' : $out_priv &= 2; break; - case 'write-content' : $out_priv &= 4; break; - case 'unlock' : $out_priv &= 8; break; - case 'read-acl' : $out_priv &= 16; break; - case 'read-current-user-privilege-set' : $out_priv &= 32; break; - case 'bind' : $out_priv &= 64; break; - case 'unbind' : $out_priv &= 128; break; - case 'write-acl' : $out_priv &= 256; break; - case 'read-free-busy' : $out_priv &= 4608; break; // 512 + 4096 - case 'schedule-deliver' : $out_priv &= 7168; break; // 1024 + 2048 + 4096 - case 'schedule-deliver-invite' : $out_priv &= 1024; break; - case 'schedule-deliver-reply' : $out_priv &= 2048; break; - case 'schedule-query-freebusy' : $out_priv &= 4096; break; - case 'schedule-send' : $out_priv &= 57344; break; // 8192 + 16384 + 32768 - case 'schedule-send-invite' : $out_priv &= 8192; break; - case 'schedule-send-reply' : $out_priv &= 16384; break; - case 'schedule-send-freebusy' : $out_priv &= 32768; break; + case 'read' : $out_priv |= 4609; break; // 1 + 512 + 4096 + case 'write' : $out_priv |= 198; break; // 2 + 4 + 64 + 128 + case 'write-properties' : $out_priv |= 2; break; + case 'write-content' : $out_priv |= 4; break; + case 'unlock' : $out_priv |= 8; break; + case 'read-acl' : $out_priv |= 16; break; + case 'read-current-user-privilege-set' : $out_priv |= 32; break; + case 'bind' : $out_priv |= 64; break; + case 'unbind' : $out_priv |= 128; break; + case 'write-acl' : $out_priv |= 256; break; + case 'read-free-busy' : $out_priv |= 4608; break; // 512 + 4096 + case 'schedule-deliver' : $out_priv |= 7168; break; // 1024 + 2048 + 4096 + case 'schedule-deliver-invite' : $out_priv |= 1024; break; + case 'schedule-deliver-reply' : $out_priv |= 2048; break; + case 'schedule-query-freebusy' : $out_priv |= 4096; break; + case 'schedule-send' : $out_priv |= 57344; break; // 8192 + 16384 + 32768 + case 'schedule-send-invite' : $out_priv |= 8192; break; + case 'schedule-send-reply' : $out_priv |= 16384; break; + case 'schedule-send-freebusy' : $out_priv |= 32768; break; default: dbg_error_log( 'ERROR', 'Cannot convert privilege of "%s" into bits.', $priv ); @@ -123,7 +123,7 @@ class DAVResource /** * @var The actual resource content, if it exists and is not a collection */ - protected $content; + protected $resource; /** * @var The type of the resource, possibly multiple @@ -195,7 +195,7 @@ class DAVResource $this->exists = null; $this->dav_name = null; $this->unique_tag = null; - $this->content = null; + $this->resource = null; $this->collection = null; $this->principal = null; $this->resourcetype = null; @@ -231,7 +231,7 @@ class DAVResource $this->exists = true; foreach( $row AS $k => $v ) { - dbg_error_log( 'resource', 'Processing resource property "%s" has "%s".', $row->dav_name, $k ); + dbg_error_log( 'DAVResource', 'Processing resource property "%s" has "%s".', $row->dav_name, $k ); switch ( $k ) { case 'dav_etag': $this->unique_tag = '"'.$v.'"'; @@ -258,16 +258,20 @@ class DAVResource $ourpath = $matches[1]. '/'; } + /** remove any leading protocol/server/port/prefix... */ + $base_path = ConstructURL('/'); + if ( preg_match( '%^(.*?)'.str_replace('%', '\\%',$base_path).'(.*)$%', $ourpath, $matches ) ) { + if ( $matches[1] == '' || $matches[1] == $c->protocol_server_port ) { + $ourpath = $matches[2]; + } + } + /** strip doubled slashes */ if ( strstr($ourpath,'//') ) $ourpath = preg_replace( '#//+#', '/', $ourpath); - /** remove any leading protocol/server/port/prefix... */ - $base_path = ConstructURL('/'); - $this->dav_name = str_replace( $base_path, '/', $ourpath ); + if ( substr($ourpath,0,1) != '/' ) $ourpath = '/'.$ourpath; - if ( substr($this->dav_name,0,1) != '/' ) { - $this->dav_name = '/'.$this->dav_name; - } + $this->dav_name = $ourpath; } @@ -300,13 +304,13 @@ class DAVResource $sql = $base_sql .'dav_name = :raw_path '; $params = array( ':raw_path' => $this->dav_name, ':session_principal' => $session->principal_id ); if ( !preg_match( '#/$#', $this->dav_name ) ) { - $sql .= ' OR dav_name = :up_to_slash OR dav_name = :plus_slash'; + $sql .= ' OR dav_name = :up_to_slash OR dav_name = :plus_slash '; $params[':up_to_slash'] = preg_replace( '#[^/]*$#', '', $this->dav_name); $params[':plus_slash'] = $this->dav_name.'/'; } $sql .= 'ORDER BY LENGTH(dav_name) DESC LIMIT 1'; $qry = new AwlQuery( $sql, $params ); - if ( $qry->Exec('DAVResource') && $qry->rows == 1 && ($row = $qry->Fetch()) ) { + if ( $qry->Exec('DAVResource') && $qry->rows() == 1 && ($row = $qry->Fetch()) ) { $this->collection = $row; if ( $row->is_calendar == 't' ) $this->collection->type = 'calendar'; @@ -331,8 +335,9 @@ EOSQL; $qry->Exec('DAVResource'); dbg_error_log( 'DAVResource', 'Created new collection as "$displayname".' ); - $qry = new AwlQuery( $base_sql . 'user_no = :user_no AND dav_name = :dav_name', $params ); - if ( $qry->Exec('DAVResource') && $qry->rows == 1 && ($row = $qry->Fetch()) ) { + $params = array( ':raw_path' => $this->dav_name, ':session_principal' => $session->principal_id ); + $qry = new AwlQuery( $base_sql . ' dav_name = :raw_path', $params ); + if ( $qry->Exec('DAVResource') && $qry->rows() == 1 && ($row = $qry->Fetch()) ) { $this->collection = $row; } } @@ -342,12 +347,6 @@ EOSQL; $this->proxy_type = $matches[3]; $this->collection->dav_name = $matches[1].'/'; } - else if ( $this->options['allow_by_email'] && preg_match( '#^/(\S+@\S+[.]\S+)/?$#', $this->dav_name, $matches) ) { - /** @TODO: perhaps we should deprecate this in favour of scheduling extensions */ - $this->collection->type = 'principal_email'; - $this->collection->dav_name = $matches[1].'/'; - $this->_is_principal = true; - } else if ( preg_match( '#^(/[^/]+)/?$#', $this->dav_name, $matches) ) { $this->collection->dav_name = $matches[1].'/'; $this->collection->type = 'principal'; @@ -362,12 +361,31 @@ EOSQL; $this->collection->dav_name = '/'; $this->collection->type = 'root'; } + else { + dbg_error_log( 'DAVResource', 'No collection for path "%s".', $this->dav_name ); + $this->collection->exists = false; + $this->collection->dav_name = preg_replace('{/[^/]*$}', '/', $this->dav_name); + } $this->_is_collection = ( $this->collection->dav_name == $this->dav_name || $this->collection->dav_name == $this->dav_name.'/' ); if ( $this->_is_collection ) { - $this->_is_calendar = $this->collection->is_calendar; - $this->_is_addressbook = $this->collection->is_addressbook; + $this->dav_name = $this->collection->dav_name; + $this->_is_calendar = ($this->collection->type == 'calendar'); + $this->_is_addressbook = ($this->collection->type == 'addressbook'); $this->contenttype = 'httpd/unix-directory'; + if ( isset($this->collection->dav_etag) ) $this->unique_tag = $this->collection->dav_etag; + if ( isset($this->collection->created) ) $this->created = $this->collection->created; + if ( isset($this->collection->modified) ) $this->modified = $this->collection->modified; + if ( isset($this->collection->resourcetype) ) + $this->resourcetype = $this->collection->resourcetype; + else { + $this->resourcetype = ''; + if ( $this->_is_principal ) + $this->resourcetype .= ''; + else { + $this->exists = (!isset($this->collection->exists) || $this->collection->exists); + } + } } } @@ -378,6 +396,13 @@ EOSQL; function FetchPrincipal() { global $c, $session; $this->principal = new CalDAVPrincipal( array( "path" => $this->dav_name ) ); + if ( $this->IsPrincipal() ) { + $this->contenttype = 'httpd/unix-directory'; + $this->unique_tag = $this->principal->dav_etag; + $this->created = $this->principal->created; + $this->modified = $this->principal->modified; + $this->resourcetype = ''; + } } @@ -398,9 +423,14 @@ EOQRY; $params = array( ':dav_name' => $this->dav_name ); $qry = new AwlQuery( $sql, $params ); - if ( $qry->Exec('DAVResource') && $qry->rows > 0 ) { + if ( $qry->Exec('DAVResource') && $qry->rows() > 0 ) { $this->exists = true; $this->resource = $qry->Fetch(); + $this->unique_tag = $this->resource->dav_etag; + $this->created = $this->resource->created; + $this->modified = $this->resource->modified; + $this->contenttype = 'text/calendar'; + $this->resourcetype = ''; } else { $this->exists = false; @@ -416,29 +446,54 @@ EOQRY; if ( $this->dav_name == '/' || $this->dav_name == '' ) { $this->privileges = 1; // read - dbg_error_log( 'DAVResource', 'Read permissions for user accessing /' ); +// dbg_error_log( 'DAVResource', 'Read permissions for user accessing /' ); return; } - if ( $session->AllowedTo('Admin') || $session->user_no == $this->user_no ) { + if ( $session->AllowedTo('Admin') ) { $this->privileges = privilege_to_bits('all'); - dbg_error_log( 'DAVResource', 'Full permissions for %s', ( $session->user_no == $this->user_no ? 'user accessing their own hierarchy' : 'an administrator') ); +// dbg_error_log( 'DAVResource', 'Full permissions for an administrator.' ); return; } + if ( $this->IsPrincipal() ) { + if ( !isset($this->principal) ) $this->FetchPrincipal(); + $this->privileges = $this->principal->Privileges(); +// dbg_error_log( 'DAVResource', 'Privileges of "%s" for user accessing principal "%s"', $this->privileges, $this->principal->username ); + return; + } + + $this->privileges = 0; if ( !isset($this->collection) ) $this->FetchCollection(); + if ( !isset($this->collection->path_privileges) ) { + $parent_path = preg_replace('{/[^/]*/$}', '/', $this->collection->dav_name ); +// dbg_error_log( 'DAVResource', 'Checking privileges of "%s" - parent of "%s"', $parent_path, $this->collection->dav_name ); + $parent = new DAVResource( $parent_path ); + + $this->collection->path_privileges = $parent->Privileges(); + } $this->privileges = $this->collection->path_privileges; } + /** + * Return the privileges bits for the current session user to this resource + */ + function Privileges() { + if ( !isset($this->privileges) ) $this->FetchPrivileges(); + return $this->privileges; + } + + /** * Is the user has the privileges to do what is requested. */ function HavePrivilegeTo( $do_what ) { if ( !isset($this->privileges) ) $this->FetchPrivileges(); $test_bits = privilege_to_bits( $do_what ); +// dbg_error_log( 'DAVResource', 'Testing privileges of "%s"(%d) against allowed "%s" => "%s"', $do_what, $test_bits, $this->privileges, ($this->privileges & $test_bits) ); return ($this->privileges & $test_bits) > 0; } @@ -454,7 +509,7 @@ EOQRY; if ( !isset($xmldoc) && isset($GLOBALS['reply']) ) $xmldoc = $GLOBALS['reply']; $privileges = array(); foreach( $privilege_names AS $k ) { - dbg_error_log( 'DAVResource', 'Adding privilege "%s".', $k ); +// dbg_error_log( 'DAVResource', 'Adding privilege "%s".', $k ); $privilege = new XMLElement('privilege'); if ( isset($xmldoc) ) $xmldoc->NSElement($privilege,$k); @@ -572,15 +627,56 @@ EOQRY; } + /** + * Checks whether this resource is a collection + */ + function IsCollection() { + return $this->_is_collection; + } + + + /** + * Checks whether this resource is a principal + */ + function IsPrincipal() { + return $this->_is_collection; + } + + + /** + * Checks whether this resource is a calendar + */ + function IsCalendar() { + return $this->_is_calendar; + } + + + /** + * Checks whether this resource is an addressbook + */ + function IsAddressbook() { + return $this->_is_addressbook; + } + + /** * Checks whether this resource actually exists, in the virtual sense, within the hierarchy */ function Exists() { - if ( isset($this->exists) ) return $this->exists; - if ( isset($this->collection) && isset($this->collection->publicly_readable) && $this->collection->publicly_readable == 't' ) { - return true; + if ( ! isset($this->exists) ) { + if ( $this->IsPrincipal() ) { + if ( !isset($this->principal) ) $this->FetchPrincipal(); + $this->exists = $this->principal->Exists(); + } + else if ( $this->IsCollection() ) { + if ( !isset($this->collection) ) $this->FetchCollection(); + } + else { + if ( !isset($this->resource) ) $this->FetchResource(); + } } - return false; + dbg_error_log('DAVResource',' Checking whether "%s" exists. It would appear %s.', $this->dav_name, ($this->exists ? 'so' : 'not') ); + return $this->exists; } @@ -605,6 +701,23 @@ EOQRY; } + /** + * Returns the principal-URL for this resource + */ + function unique_tag() { + if ( isset($this->unique_tag) ) return $this->unique_tag; + if ( $this->IsCollection() && !isset($this->collection) ) { + $this->FetchCollection(); + if ( $this->IsPrincipal() && !isset($this->principal) ) $this->FetchPrincipal(); + } + else if ( !isset($this->resource) ) $this->FetchResource(); + + if ( $this->exists !== true || !isset($this->unique_tag) ) $this->unique_tag = ''; + + return $this->unique_tag; + } + + /** * Checks whether the target collection is publicly_readable */ @@ -632,7 +745,7 @@ EOQRY; else { $qry = new AwlQuery('SELECT * FROM collection WHERE dav_name = :parent_name', array( ':parent_name' => $this->collection->parent_container ) ); - if ( $qry->Exec('DAVResource') && $qry->rows > 0 && $parent = $qry->Fetch() ) { + if ( $qry->Exec('DAVResource') && $qry->rows() > 0 && $parent = $qry->Fetch() ) { if ( $parent->is_calendar == 't' ) $this->parent_container_type = 'calendar'; else if ( $parent->is_addressbook == 't' ) @@ -649,15 +762,56 @@ EOQRY; } + /** + * Return general server-related properties, in plain form + */ + function GetProperty( $name ) { + global $c, $session; + +// dbg_error_log( 'DAVResource', 'Processing "%s".', $name ); + $value = null; + + switch( $name ) { + case 'collection_id': + if ( !isset($this->collection) ) $this->FetchCollection(); + return $this->collection->collection_id; + break; + + default: + if ( $this->_is_principal ) { + if ( !isset($this->principal) ) $this->FetchPrincipal(); + if ( isset($this->principal->{$name}) ) return $this->principal->{$name}; + if ( isset($this->collection->{$name}) ) return $this->collection->{$name}; + } + else if ( $this->_is_collection ) { + if ( !isset($this->collection) ) $this->FetchCollection(); + if ( isset($this->collection->{$name}) ) return $this->collection->{$name}; + if ( isset($this->principal->{$name}) ) return $this->principal->{$name}; + } + else { + if ( !isset($this->resource) ) $this->FetchResource(); + if ( isset($this->resource->{$name}) ) return $this->resource->{$name}; + if ( !isset($this->principal) ) $this->FetchPrincipal(); + if ( isset($this->principal->{$name}) ) return $this->principal->{$name}; + if ( !isset($this->collection) ) $this->FetchCollection(); + if ( isset($this->collection->{$name}) ) return $this->collection->{$name}; + } + dbg_error_log( 'ERROR', 'Request for property "%s" which is not understood.', $name ); + } + + return $value; + } + + /** * Return general server-related properties for this URL */ - function ResourceProperty( $tag, $prop, $reply = null ) { + function ResourceProperty( $tag, $prop, $reply = null, &$denied ) { global $c, $session; if ( $reply === null ) $reply = $GLOBALS['reply']; - dbg_error_log( 'resource', 'Processing "%s" on "%s".', $tag, $this->dav_name ); + dbg_error_log( 'DAVResource', 'Processing "%s" on "%s".', $tag, $this->dav_name ); switch( $tag ) { case 'DAV::href': @@ -677,7 +831,7 @@ EOQRY; break; case 'DAV::getlastmodified': - $prop->NewElement('getlastmodified', $this->last_modified ); + $prop->NewElement('getlastmodified', $this->modified ); break; case 'DAV::creationdate': @@ -703,7 +857,7 @@ EOQRY; case 'DAV::getetag': if ( $this->_is_collection ) { - $not_found[] = $reply->Tag($tag); + return false; } else { $prop->NewElement('getetag', $this->unique_tag ); @@ -719,17 +873,22 @@ EOQRY; $prop->NewElement('http://calendarserver.org/ns/:getctag', $this->unique_tag ); } else { - $not_found[] = $reply->Tag($tag); + return false; } break; case 'urn:ietf:params:xml:ns:caldav:calendar-data': - if ( isset($this->caldav_data) ) { + if ( $this->_is_collection ) { + if ( !isset($this->resource) ) $this->FetchResource(); + $reply->CalDAVElement($prop, $k, $this->resource->caldav_data ); + } + else { + return false; } break; default: - dbg_error_log( 'resource', 'Request for unsupported property "%s" of path "%s".', $tag, $this->dav_name ); + dbg_error_log( 'DAVResource', 'Request for unsupported property "%s" of path "%s".', $tag, $this->dav_name ); return false; } return true; @@ -746,15 +905,15 @@ EOQRY; function GetPropStat( $properties ) { global $session, $c, $request, $reply; - dbg_error_log('resource',': GetPropStat: href "%s"', $this->dav_name ); + dbg_error_log('DAVResource',': 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->dav_name, $k, $tag ); - if ( ! $this->ResourceProperty($tag, $prop, $reply) ) { - dbg_error_log( 'resource', 'Request for unsupported property "%s" of resource "%s".', $tag, $this->dav_name ); +// dbg_error_log( 'DAVResource', 'Looking at resource "%s" for property [%s]"%s".', $this->dav_name, $k, $tag ); + if ( ! $this->ResourceProperty($tag, $prop, $reply, $denied ) ) { + dbg_error_log( 'DAVResource', 'Request for unsupported property "%s" of resource "%s".', $tag, $this->dav_name ); $not_found[] = $reply->Tag($tag); } } @@ -795,7 +954,7 @@ EOQRY; function RenderAsXML( $properties, &$reply, $props_only = false ) { global $session, $c, $request; - dbg_error_log('principal',': RenderAsXML: Principal "%s"', $this->username ); + dbg_error_log('DAVResource',': RenderAsXML: Principal "%s"', $this->username ); $prop = new XMLElement('prop'); $denied = array(); @@ -812,7 +971,7 @@ EOQRY; $status = new XMLElement('status', 'HTTP/1.1 200 OK' ); $propstat = new XMLElement( 'propstat', array( $prop, $status) ); - $href = $reply->href( ConstructURL($this->dav_name) ); /** TODO: make ::href() into an accessor */ + $href = $reply->href( ConstructURL($this->dav_name) ); /** @TODO: make ::href() into an accessor */ $elements = array($href,$propstat); diff --git a/inc/HTTPAuthSession.php b/inc/HTTPAuthSession.php index 963b54a2..57c2b409 100644 --- a/inc/HTTPAuthSession.php +++ b/inc/HTTPAuthSession.php @@ -23,25 +23,25 @@ class HTTPAuthSession { * User ID number * @var user_no int */ - var $user_no; + public $user_no; /** * User e-mail * @var email string */ - var $email; + public $email; /** * User full name * @var fullname string */ - var $fullname; + public $fullname; /** * Group rights * @var groups array */ - var $groups; + public $groups; /**#@-*/ /** @@ -228,7 +228,7 @@ class HTTPAuthSession { if (isset($c->authenticate_hook['optional']) && $c->authenticate_hook['optional']) { if ($hook_response !== false) { return $hook_response; } } else { - return $hook_response; + return $hook_response; } } diff --git a/inc/always.php b/inc/always.php index 09e73e02..381cd171 100644 --- a/inc/always.php +++ b/inc/always.php @@ -68,7 +68,7 @@ if ( !isset($_SERVER['SERVER_NAME']) ) { /** * Calculate the simplest form of reference to this page, excluding the PATH_INFO following the script name. */ -$c->protocol_server_port_script = sprintf( '%s://%s%s%s', +$c->protocol_server_port = sprintf( '%s://%s%s', (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on'? 'https' : 'http'), $_SERVER['SERVER_NAME'], ( @@ -76,8 +76,8 @@ $c->protocol_server_port_script = sprintf( '%s://%s%s%s', || (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' && $_SERVER['SERVER_PORT'] == 443 ) ? '' : ':'.$_SERVER['SERVER_PORT'] - ), - ($_SERVER['SCRIPT_NAME'] == '/index.php' ? '' : $_SERVER['SCRIPT_NAME']) ); + ) ); +$c->protocol_server_port_script = $c->protocol_server_port . ($_SERVER['SCRIPT_NAME'] == '/index.php' ? '' : $_SERVER['SCRIPT_NAME']); init_gettext( 'davical', '../locale' ); @@ -167,7 +167,11 @@ function getUserByName( $username, $use_cache = true ) { // Provide some basic caching in case this ends up being overused. if ( $use_cache && isset( $_known_users_name[$username] ) ) return $_known_users_name[$username]; - $qry = new PgQuery( "SELECT *, to_char(updated at time zone 'GMT','Dy, DD Mon IYYY HH24:MI:SS \"GMT\"') AS modified FROM usr WHERE lower(username) = lower(?) ", $username ); + global $session; + if ( isset($session->user_no) ) + $qry = new PgQuery( "SELECT *, to_char(updated at time zone 'GMT','Dy, DD Mon IYYY HH24:MI:SS \"GMT\"') AS modified, principal.*, user_privileges(?,usr.user_no) AS privileges FROM usr LEFT JOIN principal USING(user_no) WHERE lower(username) = lower(?) ", $session->user_no, $username ); + else + $qry = new PgQuery( "SELECT *, to_char(updated at time zone 'GMT','Dy, DD Mon IYYY HH24:MI:SS \"GMT\"') AS modified, principal.*, 0::BIT(24) AS privileges FROM usr LEFT JOIN principal USING(user_no) WHERE lower(username) = lower(?) ", $username ); if ( $qry->Exec('always',__LINE__,__FILE__) && $qry->rows == 1 ) { $_known_users_name[$username] = $qry->Fetch(); $id = $_known_users_name[$username]->user_no; @@ -188,7 +192,8 @@ function getUserByID( $user_no, $use_cache = true ) { // Provide some basic caching in case this ends up being overused. if ( $use_cache && isset( $_known_users_id[$user_no] ) ) return $_known_users_id[$user_no]; - $qry = new PgQuery( "SELECT *, to_char(updated at time zone 'GMT','Dy, DD Mon IYYY HH24:MI:SS \"GMT\"') AS modified FROM usr WHERE user_no = ? ", intval($user_no) ); + global $session; + $qry = new PgQuery( "SELECT *, to_char(updated at time zone 'GMT','Dy, DD Mon IYYY HH24:MI:SS \"GMT\"') AS modified, principal.*, user_privileges(?,usr.user_no) AS privileges FROM usr LEFT JOIN principal USING(user_no) WHERE user_no = ? ", $session->user_no, intval($user_no) ); if ( $qry->Exec('always',__LINE__,__FILE__) && $qry->rows == 1 ) { $_known_users_id[$user_no] = $qry->Fetch(); $name = $_known_users_id[$user_no]->username; diff --git a/inc/always.php.in b/inc/always.php.in index 5231f672..21f6bcc2 100644 --- a/inc/always.php.in +++ b/inc/always.php.in @@ -68,7 +68,7 @@ if ( !isset($_SERVER['SERVER_NAME']) ) { /** * Calculate the simplest form of reference to this page, excluding the PATH_INFO following the script name. */ -$c->protocol_server_port_script = sprintf( '%s://%s%s%s', +$c->protocol_server_port = sprintf( '%s://%s%s', (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on'? 'https' : 'http'), $_SERVER['SERVER_NAME'], ( @@ -76,8 +76,8 @@ $c->protocol_server_port_script = sprintf( '%s://%s%s%s', || (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' && $_SERVER['SERVER_PORT'] == 443 ) ? '' : ':'.$_SERVER['SERVER_PORT'] - ), - ($_SERVER['SCRIPT_NAME'] == '/index.php' ? '' : $_SERVER['SCRIPT_NAME']) ); + ) ); +$c->protocol_server_port_script = $c->protocol_server_port . ($_SERVER['SCRIPT_NAME'] == '/index.php' ? '' : $_SERVER['SCRIPT_NAME']); init_gettext( 'davical', '../locale' ); @@ -167,7 +167,11 @@ function getUserByName( $username, $use_cache = true ) { // Provide some basic caching in case this ends up being overused. if ( $use_cache && isset( $_known_users_name[$username] ) ) return $_known_users_name[$username]; - $qry = new PgQuery( "SELECT *, to_char(updated at time zone 'GMT','Dy, DD Mon IYYY HH24:MI:SS \"GMT\"') AS modified FROM usr WHERE lower(username) = lower(?) ", $username ); + global $session; + if ( isset($session->user_no) ) + $qry = new PgQuery( "SELECT *, to_char(updated at time zone 'GMT','Dy, DD Mon IYYY HH24:MI:SS \"GMT\"') AS modified, principal.*, user_privileges(?,usr.user_no) AS privileges FROM usr LEFT JOIN principal USING(user_no) WHERE lower(username) = lower(?) ", $session->user_no, $username ); + else + $qry = new PgQuery( "SELECT *, to_char(updated at time zone 'GMT','Dy, DD Mon IYYY HH24:MI:SS \"GMT\"') AS modified, principal.*, 0::BIT(24) AS privileges FROM usr LEFT JOIN principal USING(user_no) WHERE lower(username) = lower(?) ", $username ); if ( $qry->Exec('always',__LINE__,__FILE__) && $qry->rows == 1 ) { $_known_users_name[$username] = $qry->Fetch(); $id = $_known_users_name[$username]->user_no; @@ -188,7 +192,8 @@ function getUserByID( $user_no, $use_cache = true ) { // Provide some basic caching in case this ends up being overused. if ( $use_cache && isset( $_known_users_id[$user_no] ) ) return $_known_users_id[$user_no]; - $qry = new PgQuery( "SELECT *, to_char(updated at time zone 'GMT','Dy, DD Mon IYYY HH24:MI:SS \"GMT\"') AS modified FROM usr WHERE user_no = ? ", intval($user_no) ); + global $session; + $qry = new PgQuery( "SELECT *, to_char(updated at time zone 'GMT','Dy, DD Mon IYYY HH24:MI:SS \"GMT\"') AS modified, principal.*, user_privileges(?,usr.user_no) AS privileges FROM usr LEFT JOIN principal USING(user_no) WHERE user_no = ? ", $session->user_no, intval($user_no) ); if ( $qry->Exec('always',__LINE__,__FILE__) && $qry->rows == 1 ) { $_known_users_id[$user_no] = $qry->Fetch(); $name = $_known_users_id[$user_no]->username; diff --git a/inc/caldav-MOVE.php b/inc/caldav-MOVE.php index 264614a0..5b3242c3 100644 --- a/inc/caldav-MOVE.php +++ b/inc/caldav-MOVE.php @@ -10,6 +10,8 @@ */ dbg_error_log("MOVE", "method handler"); +require_once('DAVResource.php'); + if ( ! $request->AllowedTo("read") ) { $request->DoResponse(403); } @@ -28,18 +30,26 @@ if ( $request->path == '/' || $request->IsPrincipal() || $request->destination = $request->DoResponse( 403 ); } -if ( !class_exists('DAVResource') ) require('DAVResource.php'); $dest = new DAVResource($request->destination); -if ( $dest->path == '/' || $dest->IsPrincipal() ) { +if ( $dest->dav_name() == '/' || $dest->IsPrincipal() ) { $request->DoResponse( 403 ); } if ( ! $request->overwrite && $dest->Exists() ) { - $request->DoResponse( 412 ); + $request->DoResponse( 412, translate('Not overwriting existing destination resource') ); } -if ( $request->IsCollection() ) { +if ( isset($request->etag_none_match) && $request->etag_none_match != '*' ) { + $request->DoResponse( 412 ); /** request to move, but only if there is no source? WTF! */ +} + +$src = new DAVResource($request->path); +if ( ! $src->Exists() ) { + $request->DoResponse( 412, translate('Source resource does not exist.') ); +} + +if ( $src->IsCollection() ) { switch( $dest->ContainerType() ) { case 'calendar': case 'addressbook': @@ -48,14 +58,41 @@ if ( $request->IsCollection() ) { $request->DoResponse( 412, translate('Special collections may not contain a calendar or other special collection.') ); }; } +else { + if ( (isset($request->etag_if_match) && $request->etag_if_match != '' ) + || ( isset($request->etag_none_match) && $request->etag_none_match != '') ) { -if ( ! $request->AllowedTo('delete') ) $request->DoResponse( 403 ); -if ( ! $dest->HaveRightsTo('DAV::write') ) $request->DoResponse( 403 ); -if ( ! $dest->Exists() && !$dest->HaveRightsTo('DAV::bind') ) $request->DoResponse( 403 ); -// if ( ! $request->HaveRightsTo('DAV::unbind') ) $request->DoResponse( 403 ); + /** + * RFC2068, 14.25: + * If none of the entity tags match, or if "*" is given and no current + * entity exists, the server MUST NOT perform the requested method, and + * MUST return a 412 (Precondition Failed) response. + * + * RFC2068, 14.26: + * If any of the entity tags match the entity tag of the entity that + * would have been returned in the response to a similar GET request + * (without the If-None-Match header) on that resource, or if "*" is + * given and any current entity exists for that resource, then the + * server MUST NOT perform the requested method. + */ + $error = ''; + if ( isset($request->etag_if_match) && $request->etag_if_match != $src->unique_tag() ) { + $error = translate( 'Existing resource does not match "If-Match" header - not accepted.'); + } + else if ( isset($request->etag_none_match) && $request->etag_none_match != '' && $request->etag_none_match == $src->unique_tag() ) { + $error = translate( 'Existing resource matches "If-None-Match" header - not accepted.'); + } + if ( $error != '' ) $request->DoResponse( 412, $error ); + } +} + +if ( ! $src->HavePrivilegeTo('DAV::unbind') ) $request->DoResponse( 403 ); +if ( ! $dest->HavePrivilegeTo('DAV::write') ) $request->DoResponse( 403 ); +if ( ! $dest->Exists() && !$dest->HavePrivilegeTo('DAV::bind') ) $request->DoResponse( 403 ); function rollback( $response_code = 412 ) { + global $request; $qry = new AwlQuery('ROLLBACK'); $qry->Exec('move'); // Just in case $request->DoResponse( $response_code ); @@ -66,21 +103,73 @@ function rollback( $response_code = 412 ) { $qry = new AwlQuery('BEGIN'); if ( !$qry->Exec('move') ) rollback(500); -if ( $request->IsCollection() ) { +$src_name = $src->dav_name(); +$dst_name = $dest->dav_name(); +$src_collection = $src->GetProperty('collection_id'); +$dst_collection = $dest->GetProperty('collection_id'); +$src_user_no = $src->GetProperty('user_no'); +$dst_user_no = $dest->GetProperty('user_no'); + + +if ( $src->IsCollection() ) { + if ( $dest->Exists() ) { + $qry = new AwlQuery( 'DELETE FROM collection WHERE dav_name = :dst_name', array( ':dst_name' => $dst_name ) ); + if ( !$qry->Exec('move') ) rollback(500); + } /** @TODO: Need to confirm this will work correctly if we move this into another user's hierarchy. */ - $qry = new AwlQuery( 'UPDATE collection SET dav_name = :new_dav_name WHERE collection_id = :collection_id', array( - ':new_dav_name' => $dest->dav_name(), - ':collection_id' => $request->collection - ); + $sql = 'UPDATE collection SET dav_name = :dst_name '; + $params = array(':dst_name' => $dst_name); + if ( $src_user_no != $dst_user_no ) { + $sql .= ', user_no = :dst_user_no'; + $params[':dst_user_no'] = $dst_user_no; + } + $sql .= 'WHERE collection_id = :src_collection'; + $params[':src_collection'] = $src_collection; + $qry = new AwlQuery( $sql, $params ); + if ( !$qry->Exec('move') ) rollback(500); } else { - $qry = new AwlQuery( 'UPDATE caldav_data SET dav_name = :new_dav_name WHERE dav_name = :old_dav_name', array( - ':old_dav_name' => $request->dav_name(), - ':new_dav_name' => $dest->dav_name() - ); + if ( $dest->Exists() ) { + $qry = new AwlQuery( 'DELETE FROM caldav_data WHERE dav_name = :dst_name', array( ':dst_name' => $dst_name) ); + if ( !$qry->Exec('move') ) rollback(500); + } + $sql = 'UPDATE caldav_data SET dav_name = :dst_name'; + $params = array( ':dst_name' => $dst_name ); + if ( $src_user_no != $dst_user_no ) { + $sql .= ', user_no = :dst_user_no'; + $params[':dst_user_no'] = $dst_user_no; + } + if ( $src_collection != $dst_collection ) { + $sql .= ', collection_id = :dst_collection'; + $params[':dst_collection'] = $dst_collection; + } + $sql .=' WHERE dav_name = :src_name'; + $params[':src_name'] = $src_name; + $qry = new AwlQuery( $sql, $params ); + if ( !$qry->Exec('move') ) rollback(500); + + $qry = new AwlQuery( 'SELECT write_sync_change( :src_collection, 404, :src_name );', array( + ':src_name' => $src_name, + ':src_collection' => $src_collection + ) ); + if ( !$qry->Exec('move') ) rollback(500); + if ( function_exists('log_caldav_action') ) { + log_caldav_action( 'DELETE', $src->GetProperty('uid'), $src_user_no, $src_collection, $src_name ); + } + + $qry = new AwlQuery( 'SELECT write_sync_change( :dst_collection, :sync_type, :dst_name );', array( + ':dst_name' => $dst_name, + ':dst_collection' => $dst_collection, + ':sync_type' => ( $dest->Exists() ? 200 : 201 ) + ) ); + if ( !$qry->Exec('move') ) rollback(500); + if ( function_exists('log_caldav_action') ) { + log_caldav_action( ( $dest->Exists() ? 'UPDATE' : 'INSERT' ), $src->GetProperty('uid'), $dst_user_no, $dst_collection, $dst_name ); + } + } $qry = new PgQuery('COMMIT'); if ( !$qry->Exec('move') ) rollback(500); -$request->DoResponse( ($put_action_type == 'INSERT' ? 201 : 204) ); +$request->DoResponse( 200 ); diff --git a/inc/caldav-OPTIONS.php b/inc/caldav-OPTIONS.php index d0b153b0..6fcb4845 100644 --- a/inc/caldav-OPTIONS.php +++ b/inc/caldav-OPTIONS.php @@ -4,9 +4,9 @@ * * @package davical * @subpackage caldav -* @author Andrew McMillan -* @copyright Catalyst .Net Ltd -* @license http://gnu.org/copyleft/gpl.html GNU GPL v2 +* @author Andrew McMillan +* @copyright Catalyst .Net Ltd, Morphoss Ltd +* @license http://gnu.org/copyleft/gpl.html GNU GPL v2 or later */ dbg_error_log("OPTIONS", "method handler"); @@ -53,8 +53,11 @@ if ( !$exists ) { */ if ( isset($c->override_allowed_methods) ) $allowed = $c->override_allowed_methods; +else if ( isset($request->supported_methods) ) { + $allowed = implode( ', ', array_keys($request->supported_methods) ); +} else { - $allowed = "OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, MKCOL, MKCALENDAR, LOCK, UNLOCK, REPORT, PROPPATCH, POST"; + $allowed = "OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, MKCOL, MKCALENDAR, LOCK, UNLOCK, REPORT, PROPPATCH, POST, MOVE"; if ( $request->path == '/' ) { $exists = true; $allowed = "OPTIONS, GET, HEAD, PROPFIND, REPORT"; @@ -65,4 +68,3 @@ header( "Allow: $allowed"); $request->DoResponse( 200, "" ); -?> diff --git a/inc/caldav-PUT-functions.php b/inc/caldav-PUT-functions.php index 39516dad..56c0b199 100644 --- a/inc/caldav-PUT-functions.php +++ b/inc/caldav-PUT-functions.php @@ -365,9 +365,9 @@ function import_collection( $ics_content, $user_no, $path, $caldav_context ) { } $sql .= <<GetPValue('UID'), $dtstamp, @@ -417,13 +417,13 @@ function putCalendarResource( &$request, $author, $caldav_context ) { * entity exists, the server MUST NOT perform the requested method, and * MUST return a 412 (Precondition Failed) response. */ - rollback_on_error( $caldav_context, $request->user_no, $request->path, 412, translate('Resource changed on server - not changed.') ); + rollback_on_error( $caldav_context, $request->user_no, $request->path, translate('Resource changed on server - not changed.'), 412 ); } $put_action_type = 'INSERT'; if ( ! $request->AllowedTo('create') ) { - rollback_on_error( $caldav_context, $request->user_no, $request->path, 403, translate('You may not add entries to this calendar.') ); + rollback_on_error( $caldav_context, $request->user_no, $request->path, translate('You may not add entries to this calendar.'), 403 ); } } elseif ( $qry->rows == 1 ) { @@ -447,7 +447,7 @@ function putCalendarResource( &$request, $author, $caldav_context ) { if ( isset($request->etag_if_match) && $request->etag_if_match != $icalendar->dav_etag ) { $error = translate( 'Existing resource does not match "If-Match" header - not accepted.'); } - if ( isset($etag_none_match) && $etag_none_match != '' && ($etag_none_match == $icalendar->dav_etag || $etag_none_match == '*') ) { + if ( isset($request->etag_none_match) && $request->etag_none_match != '' && ($request->etag_none_match == $icalendar->dav_etag || $request->etag_none_match == '*') ) { $error = translate( 'Existing resource matches "If-None-Match" header - not accepted.'); } $request->DoResponse( 412, $error ); @@ -490,12 +490,21 @@ function write_resource( $user_no, $path, $caldav_data, $collection_id, $author, global $tz_regex; $resources = $ic->GetComponents('VTIMEZONE',false); // Not matching VTIMEZONE - $first = $resources[0]; + if ( !isset($resources[0]) ) { + $resource_type = 'Unknown'; + /** @TODO: Handle writing non-calendar resources, like address book entries or random file data */ + rollback_on_error( $caldav_context, $user_no, $path, translate('No calendar content'), 412 ); + return false; + } + else { + $first = $resources[0]; + $resource_type = $first->GetType(); + } if ( $put_action_type == 'INSERT' ) { create_scheduling_requests($vcal); $qry = new PgQuery( 'BEGIN; INSERT INTO caldav_data ( user_no, dav_name, dav_etag, caldav_data, caldav_type, logged_user, created, modified, collection_id ) VALUES( ?, ?, ?, ?, ?, ?, current_timestamp, current_timestamp, ? )', - $user_no, $path, $etag, $caldav_data, $first->GetType(), $author, $collection_id ); + $user_no, $path, $etag, $caldav_data, $resource_type, $author, $collection_id ); if ( !$qry->Exec('PUT') ) { rollback_on_error( $caldav_context, $user_no, $path); return false; @@ -504,7 +513,7 @@ function write_resource( $user_no, $path, $caldav_data, $collection_id, $author, else { update_scheduling_requests($vcal); $qry = new PgQuery( 'BEGIN;UPDATE caldav_data SET caldav_data=?, dav_etag=?, caldav_type=?, logged_user=?, modified=current_timestamp WHERE user_no=? AND dav_name=?', - $caldav_data, $etag, $first->GetType(), $author, $user_no, $path ); + $caldav_data, $etag, $resource_type, $author, $user_no, $path ); if ( !$qry->Exec('PUT') ) { rollback_on_error( $caldav_context, $user_no, $path); return false; @@ -639,11 +648,11 @@ EOSQL; } else { $sql .= <<