diff --git a/htdocs/always.php b/htdocs/always.php index 69a6393d..b5c31217 100644 --- a/htdocs/always.php +++ b/htdocs/always.php @@ -18,11 +18,13 @@ unset($session); unset($request); unset($dbconn); unset($_awl_dbconn); unset($in // An ultra-simple exception handler to catch errors that occur // before we get a more functional exception handler in place... function early_exception_handler($e) { - echo "Uncaught early exception: ", $e->getMessage(), "\nAt line ", $e->getLine(), " of ", $e->getFile(), "\n"; + echo "Exception [".$e->getCode()."] ".$e->getmessage()."\n"; + echo "At line ", $e->getLine(), " of ", $e->getFile(), "\n"; + echo "================= Stack Trace ===================\n"; $trace = array_reverse($e->getTrace()); foreach( $trace AS $k => $v ) { - printf( "=====================================================\n%s[%d] %s%s%s()\n", $v['file'], $v['line'], (isset($v['class'])?$v['class']:''), (isset($v['type'])?$v['type']:''), (isset($v['function'])?$v['function']:'') ); + printf( "%s[%d] %s%s%s()\n", $v['file'], $v['line'], (isset($v['class'])?$v['class']:''), (isset($v['type'])?$v['type']:''), (isset($v['function'])?$v['function']:'') ); } } set_exception_handler('early_exception_handler'); @@ -75,7 +77,8 @@ $c->dbg = array(); if ( ! @include_once('AWLUtilities.php') ) { $try_paths = array( '../../awl/inc' - , '/usr/share/awl/inc' + , '/usr/share/awl/inc' // Where it ends up on Debian + , '/usr/share/php/awl/inc' // Fedora's standard for PHP libraries , '/usr/local/share/awl/inc' ); foreach( $try_paths AS $awl_include_path ) { @@ -169,7 +172,7 @@ init_gettext( 'davical', $c->locale_path ); * */ $c->code_version = 0; -$c->want_awl_version = '0.46'; +$c->want_awl_version = '0.47'; $c->version_string = '0.9.9.4'; // The actual version # is replaced into that during the build /release process if ( isset($c->version_string) && preg_match( '/(\d+)\.(\d+)\.(\d+)(.*)/', $c->version_string, $matches) ) { $c->code_major = $matches[1]; @@ -336,8 +339,9 @@ function ISODateToHTTPDate( $isodate ) { * Convert a date into ISO format into the sparkly new ISO format. * @param string $indate The date to convert */ -function DateToISODate( $indate ) { +function DateToISODate( $indate, $in_utc=false ) { // Use strtotime since strptime is not available on Windows platform. + if ( $in_utc ) return( gmdate('Ymd\THis\Z', strtotime($indate)) ); return( date('c', strtotime($indate)) ); } diff --git a/inc/DAVPrincipal.php b/inc/DAVPrincipal.php index 6cbcc93c..694df9c6 100644 --- a/inc/DAVPrincipal.php +++ b/inc/DAVPrincipal.php @@ -492,11 +492,11 @@ class DAVPrincipal extends Principal break; case 'urn:ietf:params:xml:ns:caldav:schedule-inbox-URL': - $reply->CalDAVElement($prop, 'schedule-inbox-URL', $reply->href($this->url('schedule_inbox')) ); + $reply->CalDAVElement($prop, 'schedule-inbox-URL', $reply->href($this->url('schedule-inbox')) ); break; case 'urn:ietf:params:xml:ns:caldav:schedule-outbox-URL': - $reply->CalDAVElement($prop, 'schedule-outbox-URL', $reply->href($this->url('schedule_outbox')) ); + $reply->CalDAVElement($prop, 'schedule-outbox-URL', $reply->href($this->url('schedule-outbox')) ); break; case 'http://calendarserver.org/ns/:dropbox-home-URL': diff --git a/inc/DAVResource.php b/inc/DAVResource.php index 7c7834d4..cc7a366a 100644 --- a/inc/DAVResource.php +++ b/inc/DAVResource.php @@ -467,7 +467,7 @@ EOSQL; /** * Find the collection associated with this resource. */ - function FetchCollection() { + protected function FetchCollection() { global $session; /** @@ -536,7 +536,7 @@ EOSQL; if ( isset($this->collection->dav_displayname) ) $this->collection->displayname = $this->collection->dav_displayname; } else { - if ( !isset($this->parent) ) $this->FetchParentContainer(); + if ( !isset($this->parent) ) $this->GetParentContainer(); $this->user_no = $this->parent->GetProperty('user_no'); } if ( isset($this->collection->resourcetypes) ) @@ -554,7 +554,7 @@ EOSQL; /** * Find the principal associated with this resource. */ - function FetchPrincipal() { + protected function FetchPrincipal() { if ( isset($this->principal) ) return; $this->principal = new DAVPrincipal( array( "path" => $this->bound_from() ) ); if ( $this->_is_principal ) { @@ -577,7 +577,7 @@ EOSQL; /** * Retrieve the actual resource. */ - function FetchResource() { + protected function FetchResource() { global $c, $session; if ( isset($this->exists) ) return; // True or false, we've got what we can already @@ -606,10 +606,12 @@ EOQRY; /** * Fetch any dead properties for this URL */ - function FetchDeadProperties() { + protected function FetchDeadProperties() { if ( isset($this->dead_properties) ) return; $this->dead_properties = array(); + if ( !$this->exists || !$this->_is_collection ) return; + $qry = new AwlQuery('SELECT property_name, property_value FROM property WHERE dav_name= :dav_name', array(':dav_name' => $this->dav_name) ); if ( $qry->Exec('DAVResource') ) { while ( $property = $qry->Fetch() ) { @@ -619,29 +621,10 @@ EOQRY; } - /** - * Fetch the parent to this resource. - */ - function FetchParentContainer() { - if ( $this->dav_name == '/' ) return null; - if ( !isset($this->parent) ) { - if ( $this->_is_collection ) { - dbg_error_log( 'DAVResource', 'Retrieving "%s" - parent of "%s" (dav_name: %s)', $this->parent_path(), $this->collection->dav_name, $this->dav_name() ); - $this->parent = new DAVResource( $this->parent_path() ); - } - else { - dbg_error_log( 'DAVResource', 'Retrieving "%s" - parent of "%s" (dav_name: %s)', $this->parent_path(), $this->collection->dav_name, $this->dav_name() ); - $this->parent = new DAVResource($this->collection->dav_name); - } - } - return $this->parent; - } - - /** * Build permissions for this URL */ - function FetchPrivileges() { + protected function FetchPrivileges() { global $session, $request; if ( $this->dav_name == '/' || $this->dav_name == '' ) { @@ -666,7 +649,7 @@ EOQRY; if ( ! isset($this->collection) ) $this->FetchCollection(); $this->privileges = 0; if ( !isset($this->collection->path_privs) ) { - if ( !isset($this->parent) ) $this->FetchParentContainer(); + if ( !isset($this->parent) ) $this->GetParentContainer(); $this->collection->path_privs = $this->parent->Privileges(); $this->collection->user_no = $this->parent->GetProperty('user_no'); @@ -696,6 +679,34 @@ EOQRY; } + /** + * Get a DAVResource which is the parent to this resource. + */ + function GetParentContainer() { + if ( $this->dav_name == '/' ) return null; + if ( !isset($this->parent) ) { + if ( $this->_is_collection ) { + dbg_error_log( 'DAVResource', 'Retrieving "%s" - parent of "%s" (dav_name: %s)', $this->parent_path(), $this->collection->dav_name, $this->dav_name() ); + $this->parent = new DAVResource( $this->parent_path() ); + } + else { + dbg_error_log( 'DAVResource', 'Retrieving "%s" - parent of "%s" (dav_name: %s)', $this->parent_path(), $this->collection->dav_name, $this->dav_name() ); + $this->parent = new DAVResource($this->collection->dav_name); + } + } + return $this->parent; + } + + + /** + * Fetch the parent to this resource. + */ + function FetchParentContainer() { + deprecated('DAVResource::FetchParentContainer'); + return $this->GetParentContainer(); + } + + /** * Return the privileges bits for the current session user to this resource */ @@ -1096,7 +1107,7 @@ EOQRY; if ( $this->collection->dav_name != $this->dav_name ) { return $this->collection->exists; } - $parent = $this->FetchParentContainer(); + $parent = $this->GetParentContainer(); return $parent->Exists(); } @@ -1167,6 +1178,24 @@ EOQRY; } + /** + * Returns the internal user_no for the principal for this resource + */ + function user_no() { + if ( !isset($this->principal) ) $this->FetchPrincipal(); + return $this->principal->user_no(); + } + + + /** + * Returns the internal collection_id for this collection, or the collection containing this resource + */ + function collection_id() { + if ( !isset($this->collection) ) $this->FetchCollection(); + return $this->collection->collection_id; + } + + /** * Returns the database row for this resource */ @@ -1321,7 +1350,7 @@ EOQRY; return $this->principal->principal_id(); break; - case 'resourcetype': + case 'resourcetype': if ( isset($this->resourcetypes) ) { $this->resourcetypes = preg_replace('{^\s*<(.*)/>\s*$}', '$1', $this->resourcetypes); $type_list = preg_split('{(/>\s*<|\n)}', $this->resourcetypes); @@ -1481,13 +1510,13 @@ EOQRY; break; case 'DAV::getlastmodified': - /** peculiarly, it seems that getlastmodified is HTTP Date format! */ + /** getlastmodified is HTTP Date format: i.e. the Last-Modified header in response to a GET */ $reply->NSElement($prop, $tag, ISODateToHTTPDate($this->GetProperty('modified')) ); break; case 'DAV::creationdate': - /** bizarrely, it seems that creationdate is ISO8601 format */ - $reply->NSElement($prop, $tag, DateToISODate($this->GetProperty('created')) ); + /** creationdate is ISO8601 format */ + $reply->NSElement($prop, $tag, DateToISODate($this->GetProperty('created'), true) ); break; case 'DAV::getcontentlength': diff --git a/inc/Principal.php b/inc/Principal.php index bbda7805..304f44b1 100644 --- a/inc/Principal.php +++ b/inc/Principal.php @@ -381,10 +381,13 @@ class Principal { } switch( $type ) { - case 'schedule_inbox': $result .= '.in/'; break; - case 'schedule_outbox': $result .= '.out/'; break; + case 'principal': break; + case 'schedule-inbox': $result .= '.in/'; break; + case 'schedule-outbox': $result .= '.out/'; break; case 'dropbox': $result .= '.drop/'; break; case 'notifications': $result .= '.notify/'; break; + default: + fatal('Unknown internal URL type "'.$type.'"'); } return $result; } diff --git a/inc/RRule-v2.php b/inc/RRule-v2.php index f27e8817..d6a35a45 100644 --- a/inc/RRule-v2.php +++ b/inc/RRule-v2.php @@ -11,6 +11,25 @@ if ( !class_exists('DateTime') ) return; +/** +* Try and extract something like "Pacific/Auckland" or "America/Indiana/Indianapolis" if possible, given +* the VTIMEZONE component that is passed in. This is much more complex than olson_from_tzstring since +* we start to examine the rules and work out what actual timezone this might be. +*/ +function olson_from_vtimezone( vComponent $vtz ) { + $tzid = $vtz->GetProperty('TZID'); + if ( empty($tzid) ) $tzid = $vtz->GetProperty('TZID'); + if ( !empty($tzid) ) { + $result = olson_from_tzstring($tzid); + if ( !empty($result) ) return $result; + } + + /** + * @todo: We'll do other stuff here, in due course... + */ + return null; +} + // define( 'DEBUG_RRULE', true); define( 'DEBUG_RRULE', false ); @@ -59,20 +78,24 @@ class Rfc5545Duration { private $epoch_seconds = null; private $days = 0; private $secs = 0; - private $as_text = null; + private $as_text = ''; /** * Construct a new Rfc5545Duration either from incoming seconds or a text string. * @param mixed $in_duration */ function __construct( $in_duration ) { - if ( preg_match('{^-?P(\dW)|((\dD)?(T(\dH)?(\dM)?(\dS)?)?)$}i', $subject, $matches) ) { + if ( is_integer($in_duration) ) { + $this->epoch_seconds = $in_duration; + $this->as_text = ''; + } + else if ( gettype($in_duration) == 'string' ) { +// preg_match('{^-?P(\dW)|((\dD)?(T(\dH)?(\dM)?(\dS)?)?)$}i', $in_duration, $matches) ) { $this->as_text = $in_duration; $this->epoch_seconds = null; } - elseif ( is_integer($in_duration) ) { - $this->epoch_seconds = $in_duration; - $this->as_text = null; + else { +// fatal('Passed duration is neither numeric nor string!'); } } @@ -117,11 +140,12 @@ class Rfc5545Duration { /** * Returns the duration as a text string of the form ^(-?)P(\d+W)|((\d+D)?(T(\d+H)?(\d+M)?(\d+S)?)?)$ + * @return string The stringified stuff. */ function __toString() { - if ( !isset($this->as_text) ) { - $this->as_text = ($in_duration < 0 ? '-P' : 'P'); - $in_duration = abs($in_duration); + if ( empty($this->as_text) ) { + $this->as_text = ($this->epoch_seconds < 0 ? '-P' : 'P'); + $in_duration = abs($this->epoch_seconds); if ( $in_duration >= 86400 ) { $this->days = floor($in_duration / 86400); $in_duration -= $this->days * 86400; diff --git a/inc/WritableCollection.php b/inc/WritableCollection.php index 0dd63d3b..93e38913 100644 --- a/inc/WritableCollection.php +++ b/inc/WritableCollection.php @@ -5,204 +5,230 @@ class WritableCollection extends DAVResource { /** * Writes the data to a member in the collection and returns the segment_name of the resource in our internal namespace. - * @param $data iCalendar The resource to be written. - * @param $create_resource boolean True if this is a new resource. - * @param $segment_name The name of the resource within the collection. + * @param vComponent $data The resource to be written. + * @param boolean $create_resource True if this is a new resource. + * @param $segment_name The name of the resource within the collection, or false on failure. */ - function WriteCalendarMember( $data, $create_resource, $segment_name = null ) { - if ( !$this->IsSchedulingCollection() && !$this->IsCalendar() ) return false; - -// function write_resource( $user_no, $path, $caldav_data, $collection_id, $author, $etag, $ic, $put_action_type, $caldav_context, $log_action=true, $weak_etag=null ) { - global $tz_regex; - - $resources = $ic->GetComponents('VTIMEZONE',false); // Not matching VTIMEZONE - 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(); - } - - $qry = new AwlQuery(); - $qry->Begin(); - $params = array( - ':dav_name' => $path, - ':user_no' => $user_no, - ':etag' => $etag, - ':dav_data' => $caldav_data, - ':caldav_type' => $resource_type, - ':session_user' => $author, - ':weak_etag' => $weak_etag - ); - if ( $put_action_type == 'INSERT' ) { - create_scheduling_requests($vcal); - $sql = 'INSERT INTO caldav_data ( user_no, dav_name, dav_etag, caldav_data, caldav_type, logged_user, created, modified, collection_id, weak_etag ) - VALUES( :user_no, :dav_name, :etag, :dav_data, :caldav_type, :session_user, current_timestamp, current_timestamp, :collection_id, :weak_etag )'; - $params[':collection_id'] = $collection_id; - } - else { - update_scheduling_requests($vcal); - $sql = 'UPDATE caldav_data SET caldav_data=:dav_data, dav_etag=:etag, caldav_type=:caldav_type, logged_user=:session_user, - modified=current_timestamp, weak_etag=:weak_etag WHERE user_no=:user_no AND dav_name=:dav_name'; - } - if ( !$qry->QDo($sql,$params) ) { - rollback_on_error( $caldav_context, $user_no, $path); - return false; - } - - $qry->QDo('SELECT dav_id FROM caldav_data WHERE dav_name = :dav_name ', array(':dav_name' => $path)); - if ( $qry->rows() == 1 && $row = $qry->Fetch() ) { - $dav_id = $row->dav_id; - } - - - $calitem_params = array( - ':dav_name' => $path, - ':user_no' => $user_no, - ':etag' => $etag - ); - $dtstart = $first->GetPValue('DTSTART'); - $calitem_params[':dtstart'] = $dtstart; - if ( (!isset($dtstart) || $dtstart == '') && $first->GetPValue('DUE') != '' ) { - $dtstart = $first->GetPValue('DUE'); - } - - $dtend = $first->GetPValue('DTEND'); - if ( isset($dtend) && $dtend != '' ) { - dbg_error_log( 'PUT', ' DTEND: "%s", DTSTART: "%s", DURATION: "%s"', $dtend, $dtstart, $first->GetPValue('DURATION') ); - $calitem_params[':dtend'] = $dtend; - $dtend = ':dtend'; - } - else { - $dtend = 'NULL'; - if ( $first->GetPValue('DURATION') != '' AND $dtstart != '' ) { - $duration = preg_replace( '#[PT]#', ' ', $first->GetPValue('DURATION') ); - $dtend = '(:dtstart::timestamp with time zone + :duration::interval)'; - $calitem_params[':duration'] = $duration; + function WriteCalendarMember( vComponent $data, $create_resource, $do_scheduling=true, $segment_name = null ) { + if ( !$this->IsSchedulingCollection() && !$this->IsCalendar() ) { + dbg_error_log( 'PUT', '"%s" is not a calendar or scheduling collection!', $this->dav_name); + return false; } - elseif ( $first->GetType() == 'VEVENT' ) { - /** - * From RFC2445 4.6.1: - * For cases where a "VEVENT" calendar component specifies a "DTSTART" - * property with a DATE data type but no "DTEND" property, the events - * non-inclusive end is the end of the calendar date specified by the - * "DTSTART" property. For cases where a "VEVENT" calendar component specifies - * a "DTSTART" property with a DATE-TIME data type but no "DTEND" property, - * the event ends on the same calendar date and time of day specified by the - * "DTSTART" property. - * - * So we're looking for 'VALUE=DATE', to identify the duration, effectively. - * - */ - $value_type = $first->GetPParamValue('DTSTART','VALUE'); - dbg_error_log('PUT','DTSTART without DTEND. DTSTART value type is %s', $value_type ); - if ( isset($value_type) && $value_type == 'DATE' ) - $dtend = '(:dtstart::timestamp with time zone::date + \'1 day\'::interval)'; - else - $dtend = ':dtstart'; - } - } - $last_modified = $first->GetPValue('LAST-MODIFIED'); - if ( !isset($last_modified) || $last_modified == '' ) { - $last_modified = gmdate( 'Ymd\THis\Z' ); - } - $calitem_params[':modified'] = $last_modified; + global $tz_regex, $session, $caldav_context; + + $resources = $data->GetComponents('VTIMEZONE',false); // Not matching VTIMEZONE + $user_no = $this->user_no(); + $collection_id = $this->collection_id(); - $dtstamp = $first->GetPValue('DTSTAMP'); - if ( !isset($dtstamp) || $dtstamp == '' ) { - $dtstamp = $last_modified; - } - $calitem_params[':dtstamp'] = $dtstamp; - - $class = $first->GetPValue('CLASS'); - /* Check and see if we should over ride the class. */ - /** @todo is there some way we can move this out of this function? Or at least get rid of the need for the SQL query here. */ - if ( public_events_only($user_no, $path) ) { - $class = 'PUBLIC'; - } - - /* - * It seems that some calendar clients don't set a class... - * RFC2445, 4.8.1.3: - * Default is PUBLIC - */ - if ( !isset($class) || $class == '' ) { - $class = 'PUBLIC'; - } - $calitem_params[':class'] = $class; - - - /** Calculate what timezone to set, first, if possible */ - $last_tz_locn = 'Turkmenikikamukau'; // I really hope this location doesn't exist! - $tzid = $first->GetPParamValue('DTSTART','TZID'); - if ( !isset($tzid) || $tzid == '' ) $tzid = $first->GetPParamValue('DUE','TZID'); - $timezones = $ic->GetComponents('VTIMEZONE'); - foreach( $timezones AS $k => $tz ) { - if ( $tz->GetPValue('TZID') != $tzid ) { - /** - * We'll pretend they didn't forget to give us a TZID and that they - * really hope the server is running in the timezone they supplied... but be noisy about it. - */ - dbg_error_log( 'ERROR', ' Event includes TZID[%s] but uses TZID[%s]!', $tz->GetPValue('TZID'), $tzid ); - $tzid = $tz->GetPValue('TZID'); - } - // This is the one - $tz_locn = $tz->GetPValue('X-LIC-LOCATION'); - if ( ! isset($tz_locn) ) { - if ( preg_match( '#([^/]+/[^/]+)$#', $tzid, $matches ) ) - $tz_locn = $matches[1]; - else if ( isset($tzid) && $tzid != '' ) { - dbg_error_log( 'ERROR', ' Couldn\'t guess Olsen TZ from TZID[%s]. This may end in tears...', $tzid ); - } + if ( !isset($resources[0]) ) { + rollback_on_error( $caldav_context, $user_no, $this->dav_name.'/'.$segment_name, translate('No calendar content'), 412 ); + return false; } else { - if ( ! preg_match( $tz_regex, $tz_locn ) ) { - if ( preg_match( '#([^/]+/[^/]+)$#', $tzid, $matches ) ) $tz_locn = $matches[1]; + $first = $resources[0]; + $resource_type = $first->GetType(); + } + + $uid = $first->GetPValue('UID'); + if ( empty($segment_name) ) { + $segment_name = $uid.'.ics'; + } + $path = $this->dav_name() . $segment_name; + + $caldav_data = $data->Render(); + $etag = md5($caldav_data); + $weak_etag = null; + + $qry = new AwlQuery(); + $existing_transaction_state = $qry->TransactionState(); + if ( $existing_transaction_state == 0 ) $qry->Begin(); + + + if ( $create_resource ) { + $qry->QDo('SELECT nextval(\'dav_id_seq\') AS dav_id'); + } + else { + $qry->QDo('SELECT dav_id FROM caldav_data WHERE dav_name = :dav_name ', array(':dav_name' => $path)); + } + if ( $qry->rows() != 1 || !($row = $qry->Fetch()) ) { + // No dav_id? => We're toast! + rollback_on_error( $caldav_context, $user_no, $path); + return false; + } + $dav_id = $row->dav_id; + + $calitem_params = array( + ':dav_name' => $path, + ':user_no' => $user_no, + ':etag' => $etag, + ':dav_id' => $dav_id + ); + + $dav_params = array_merge($calitem_params, array( + ':dav_data' => $caldav_data, + ':caldav_type' => $resource_type, + ':session_user' => $session->user_no, + ':weak_etag' => $weak_etag + ) ); + + if ( $create_resource ) { + if ( !$this->IsSchedulingCollection() ) create_scheduling_requests($vcal); + $sql = 'INSERT INTO caldav_data ( dav_id, user_no, dav_name, dav_etag, caldav_data, caldav_type, logged_user, created, modified, collection_id, weak_etag ) + VALUES( :dav_id, :user_no, :dav_name, :etag, :dav_data, :caldav_type, :session_user, current_timestamp, current_timestamp, :collection_id, :weak_etag )'; + $dav_params[':collection_id'] = $collection_id; + } + else { + if ( !$this->IsSchedulingCollection() ) update_scheduling_requests($vcal); + $sql = 'UPDATE caldav_data SET caldav_data=:dav_data, dav_etag=:etag, caldav_type=:caldav_type, logged_user=:session_user, + modified=current_timestamp, weak_etag=:weak_etag WHERE dav_id=:dav_id'; + } + if ( !$qry->QDo($sql,$dav_params) ) { + rollback_on_error( $caldav_context, $user_no, $path); + return false; + } + + $dtstart = $first->GetPValue('DTSTART'); + $calitem_params[':dtstart'] = $dtstart; + if ( (!isset($dtstart) || $dtstart == '') && $first->GetPValue('DUE') != '' ) { + $dtstart = $first->GetPValue('DUE'); + } + + $dtend = $first->GetPValue('DTEND'); + if ( isset($dtend) && $dtend != '' ) { + dbg_error_log( 'PUT', ' DTEND: "%s", DTSTART: "%s", DURATION: "%s"', $dtend, $dtstart, $first->GetPValue('DURATION') ); + $calitem_params[':dtend'] = $dtend; + $dtend = ':dtend'; + } + else { + $dtend = 'NULL'; + if ( $first->GetPValue('DURATION') != '' AND $dtstart != '' ) { + $duration = preg_replace( '#[PT]#', ' ', $first->GetPValue('DURATION') ); + $dtend = '(:dtstart::timestamp with time zone + :duration::interval)'; + $calitem_params[':duration'] = $duration; + } + elseif ( $first->GetType() == 'VEVENT' ) { + /** + * From RFC2445 4.6.1: + * For cases where a "VEVENT" calendar component specifies a "DTSTART" + * property with a DATE data type but no "DTEND" property, the events + * non-inclusive end is the end of the calendar date specified by the + * "DTSTART" property. For cases where a "VEVENT" calendar component specifies + * a "DTSTART" property with a DATE-TIME data type but no "DTEND" property, + * the event ends on the same calendar date and time of day specified by the + * "DTSTART" property. + * + * So we're looking for 'VALUE=DATE', to identify the duration, effectively. + * + */ + $value_type = $first->GetPParamValue('DTSTART','VALUE'); + dbg_error_log('PUT','DTSTART without DTEND. DTSTART value type is %s', $value_type ); + if ( isset($value_type) && $value_type == 'DATE' ) + $dtend = '(:dtstart::timestamp with time zone::date + \'1 day\'::interval)'; + else + $dtend = ':dtstart'; } } - - dbg_error_log( 'PUT', ' Using TZID[%s] and location of [%s]', $tzid, (isset($tz_locn) ? $tz_locn : '') ); - if ( isset($tz_locn) && ($tz_locn != $last_tz_locn) && preg_match( $tz_regex, $tz_locn ) ) { - dbg_error_log( 'PUT', ' Setting timezone to %s', $tz_locn ); - if ( $tz_locn != '' ) { - $qry->QDo('SET TIMEZONE TO \''.$tz_locn."'" ); + + $last_modified = $first->GetPValue('LAST-MODIFIED'); + if ( !isset($last_modified) || $last_modified == '' ) { + $last_modified = gmdate( 'Ymd\THis\Z' ); + } + $calitem_params[':modified'] = $last_modified; + + $dtstamp = $first->GetPValue('DTSTAMP'); + if ( !isset($dtstamp) || $dtstamp == '' ) { + $dtstamp = $last_modified; + } + $calitem_params[':dtstamp'] = $dtstamp; + + $class = $first->GetPValue('CLASS'); + /* Check and see if we should over ride the class. */ + /** @todo is there some way we can move this out of this function? Or at least get rid of the need for the SQL query here. */ + if ( public_events_only($user_no, $path) ) { + $class = 'PUBLIC'; + } + + /* + * It seems that some calendar clients don't set a class... + * RFC2445, 4.8.1.3: + * Default is PUBLIC + */ + if ( !isset($class) || $class == '' ) { + $class = 'PUBLIC'; + } + $calitem_params[':class'] = $class; + + + /** Calculate what timezone to set, first, if possible */ + $last_tz_locn = 'Turkmenikikamukau'; // I really hope this location doesn't exist! + $dtstart_prop = $first->GetProperty('DTSTART'); + $tzid = $dtstart_prop->GetParameterValue('TZID'); + if ( empty($tzid) && $first->GetType() == 'VTODO' ) { + $due_prop = $first->GetProperty('DUE'); + $tzid = $due_prop->GetParameterValue('TZID'); + } + $timezones = $data->GetComponents('VTIMEZONE'); + foreach( $timezones AS $k => $tz ) { + if ( $tz->GetPValue('TZID') != $tzid ) { + /** + * We'll skip any tz definitions that are for a TZID other than the DTSTART/DUE on the first VEVENT/VTODO + */ + dbg_error_log( 'ERROR', ' Event uses TZID[%s], skipping included TZID[%s]!', $tz->GetPValue('TZID'), $tzid ); + continue; } - $last_tz_locn = $tz_locn; + // This is the one + $tz_locn = $tz->GetPValue('X-LIC-LOCATION'); + if ( ! isset($tz_locn) ) { + if ( preg_match( '#([^/]+/[^/]+)$#', $tzid, $matches ) ) + $tz_locn = $matches[1]; + else if ( isset($tzid) && $tzid != '' ) { + dbg_error_log( 'ERROR', ' Couldn\'t guess Olsen TZ from TZID[%s]. This may end in tears...', $tzid ); + } + } + else { + if ( ! preg_match( $tz_regex, $tz_locn ) ) { + if ( preg_match( '#([^/]+/[^/]+)$#', $tzid, $matches ) ) $tz_locn = $matches[1]; + } + } + + dbg_error_log( 'PUT', ' Using TZID[%s] and location of [%s]', $tzid, (isset($tz_locn) ? $tz_locn : '') ); + if ( isset($tz_locn) && ($tz_locn != $last_tz_locn) && preg_match( $tz_regex, $tz_locn ) ) { + dbg_error_log( 'PUT', ' Setting timezone to %s', $tz_locn ); + if ( $tz_locn != '' ) { + $qry->QDo('SET TIMEZONE TO \''.$tz_locn."'" ); + } + $last_tz_locn = $tz_locn; + } + $params = array( ':tzid' => $tzid); + $qry = new AwlQuery('SELECT tz_locn FROM time_zone WHERE tz_id = :tzid', $params ); + if ( $qry->Exec('PUT',__LINE__,__FILE__) && $qry->rows() == 0 ) { + $params[':tzlocn'] = $tz_locn; + $params[':tzspec'] = (isset($tz) ? $tz->Render() : null ); + $qry->QDo('INSERT INTO time_zone (tz_id, tz_locn, tz_spec) VALUES(:tzid,:tzlocn,:tzspec)', $params ); + } + if ( !isset($tz_locn) || $tz_locn == '' ) $tz_locn = $tzid; + } - $params = array( ':tzid' => $tzid); - $qry = new AwlQuery('SELECT tz_locn FROM time_zone WHERE tz_id = :tzid', $params ); - if ( $qry->Exec('PUT',__LINE__,__FILE__) && $qry->rows() == 0 ) { - $params[':tzlocn'] = $tz_locn; - $params[':tzspec'] = (isset($tz) ? $tz->Render() : null ); - $qry->QDo('INSERT INTO time_zone (tz_id, tz_locn, tz_spec) VALUES(:tzid,:tzlocn,:tzspec)', $params ); - } - if ( !isset($tz_locn) || $tz_locn == '' ) $tz_locn = $tzid; - - } - - $created = $first->GetPValue('CREATED'); - if ( $created == '00001231T000000Z' ) $created = '20001231T000000Z'; - $calitem_params[':created'] = $created; - - $calitem_params[':tzid'] = $tzid; - $calitem_params[':uid'] = $first->GetPValue('UID'); - $calitem_params[':summary'] = $first->GetPValue('SUMMARY'); - $calitem_params[':location'] = $first->GetPValue('LOCATION'); - $calitem_params[':transp'] = $first->GetPValue('TRANSP'); - $calitem_params[':description'] = $first->GetPValue('DESCRIPTION'); - $calitem_params[':rrule'] = $first->GetPValue('RRULE'); - $calitem_params[':url'] = $first->GetPValue('URL'); - $calitem_params[':priority'] = $first->GetPValue('PRIORITY'); - $calitem_params[':due'] = $first->GetPValue('DUE'); - $calitem_params[':percent_complete'] = $first->GetPValue('PERCENT-COMPLETE'); - $calitem_params[':status'] = $first->GetPValue('STATUS'); - if ( $put_action_type == 'INSERT' ) { - $sql = <<GetPValue('CREATED'); + if ( $created == '00001231T000000Z' ) $created = '20001231T000000Z'; + $calitem_params[':created'] = $created; + + $calitem_params[':tzid'] = $tzid; + $calitem_params[':uid'] = $uid; + $calitem_params[':summary'] = $first->GetPValue('SUMMARY'); + $calitem_params[':location'] = $first->GetPValue('LOCATION'); + $calitem_params[':transp'] = $first->GetPValue('TRANSP'); + $calitem_params[':description'] = $first->GetPValue('DESCRIPTION'); + $calitem_params[':rrule'] = $first->GetPValue('RRULE'); + $calitem_params[':url'] = $first->GetPValue('URL'); + $calitem_params[':priority'] = $first->GetPValue('PRIORITY'); + $calitem_params[':due'] = $first->GetPValue('DUE'); + $calitem_params[':percent_complete'] = $first->GetPValue('PERCENT-COMPLETE'); + $calitem_params[':status'] = $first->GetPValue('STATUS'); + if ( $create_resource ) { + $sql = <<GetPValue('UID'), $user_no, $collection_id, $path ); - } - else if ( $log_action ) { - dbg_error_log( 'PUT', 'No log_caldav_action( %s, %s, %s, %s, %s) can be called.', - $put_action_type, $first->GetPValue('UID'), $user_no, $collection_id, $path ); - } + $sync_change = 200; + } - $qry = new AwlQuery( $sql, $calitem_params ); - if ( !$qry->Exec('PUT',__LINE__,__FILE__) ) { - rollback_on_error( $caldav_context, $user_no, $path); - return false; - } - $qry->QDo("SELECT write_sync_change( $collection_id, $sync_change, :dav_name)", array(':dav_name' => $path ) ); - $qry->Commit(); - - dbg_error_log( 'PUT', 'User: %d, ETag: %s, Path: %s', $author, $etag, $path); + if ( !$this->IsSchedulingCollection() ) { + write_alarms($dav_id, $first); + write_attendees($dav_id, $first); + if ( $log_action && function_exists('log_caldav_action') ) { + log_caldav_action( $put_action_type, $first->GetPValue('UID'), $user_no, $collection_id, $path ); + } + else if ( $log_action ) { + dbg_error_log( 'PUT', 'No log_caldav_action( %s, %s, %s, %s, %s) can be called.', + $put_action_type, $first->GetPValue('UID'), $user_no, $collection_id, $path ); + } + } + $qry = new AwlQuery( $sql, $calitem_params ); + if ( !$qry->Exec('PUT',__LINE__,__FILE__) ) { + rollback_on_error( $caldav_context, $user_no, $path); + return false; + } + $qry->QDo("SELECT write_sync_change( $collection_id, $sync_change, :dav_name)", array(':dav_name' => $path ) ); + if ( $existing_transaction_state == 0 ) $qry->Commit(); + + dbg_error_log( 'PUT', 'User: %d, ETag: %s, Path: %s', $session->user_no, $etag, $path); + return $segment_name; } @@ -254,14 +281,22 @@ EOSQL; * Writes the data to a member in the collection and returns the segment_name of the resource in our internal namespace. * @param $data mixed The resource to be written. * @param $create_resource boolean True if this is a new resource. - * @param $segment_name The name of the resource within the collection. + * @param $segment_name The name of the resource within the collection, or false on failure. */ function WriteMember( $data, $create_resource, $segment_name = null ) { - if ( ! $this->IsCollection() ) return false; - if ( is_object($data) ) { - if ( gettype($data) == 'iCalendar' ) return $this->WriteCalendarMember($data,$create_resource,$segment_name); - else if ( gettype($data) == 'VCard' ) return $this->WriteAddressbookMember($data,$create_resource,$segment_name); + if ( ! $this->IsCollection() ) { + dbg_error_log( 'PUT', '"%s" is not a collection path', $this->dav_name); + return false; } + if ( ! is_object($data) ) { + dbg_error_log( 'PUT', 'No data supplied!' ); + return false; + } + + if ( $data instanceof vComponent ) + return $this->WriteCalendarMember($data,$create_resource,$segment_name); + else if ( $data instanceof VCard ) + return $this->WriteAddressbookMember($data,$create_resource,$segment_name); return $segment_name; } diff --git a/inc/always.php.in b/inc/always.php.in index a1dd35e0..cf4f1ef7 100644 --- a/inc/always.php.in +++ b/inc/always.php.in @@ -18,11 +18,13 @@ unset($session); unset($request); unset($dbconn); unset($_awl_dbconn); unset($in // An ultra-simple exception handler to catch errors that occur // before we get a more functional exception handler in place... function early_exception_handler($e) { - echo "Uncaught early exception: ", $e->getMessage(), "\nAt line ", $e->getLine(), " of ", $e->getFile(), "\n"; + echo "Exception [".$e->getCode()."] ".$e->getmessage()."\n"; + echo "At line ", $e->getLine(), " of ", $e->getFile(), "\n"; + echo "================= Stack Trace ===================\n"; $trace = array_reverse($e->getTrace()); foreach( $trace AS $k => $v ) { - printf( "=====================================================\n%s[%d] %s%s%s()\n", $v['file'], $v['line'], (isset($v['class'])?$v['class']:''), (isset($v['type'])?$v['type']:''), (isset($v['function'])?$v['function']:'') ); + printf( "%s[%d] %s%s%s()\n", $v['file'], $v['line'], (isset($v['class'])?$v['class']:''), (isset($v['type'])?$v['type']:''), (isset($v['function'])?$v['function']:'') ); } } set_exception_handler('early_exception_handler'); diff --git a/inc/caldav-POST.php b/inc/caldav-POST.php index aef5b9dd..c8d9947b 100644 --- a/inc/caldav-POST.php +++ b/inc/caldav-POST.php @@ -11,7 +11,6 @@ dbg_error_log("POST", "method handler"); require_once("XMLDocument.php"); -require_once("iCalendar.php"); include_once('caldav-PUT-functions.php'); include_once('freebusy-functions.php'); @@ -97,8 +96,7 @@ function handle_freebusy_request( $ic ) { $fb->SetProperties( $ic->GetProperties('ORGANIZER'), 'ORGANIZER'); $fb->AddProperty( $attendee ); - $vcal = new iCalComponent(); - $vcal->VCalendar( array('METHOD' => 'REPLY') ); + $vcal = new vCalendar( array('METHOD' => 'REPLY') ); $vcal->AddComponent( $fb ); $response = $reply->NewXMLElement( "response", false, false, 'urn:ietf:params:xml:ns:caldav' ); @@ -124,7 +122,7 @@ function handle_cancel_request( $ic ) { $request->XMLResponse( 200, $response ); } -$ical = new iCalComponent( $request->raw_post ); +$ical = new vComponent( $request->raw_post ); $method = $ical->GetPValue('METHOD'); $resources = $ical->GetComponents('VTIMEZONE',false); diff --git a/inc/caldav-PROPFIND.php b/inc/caldav-PROPFIND.php index 7964fbd2..d48cf855 100644 --- a/inc/caldav-PROPFIND.php +++ b/inc/caldav-PROPFIND.php @@ -91,7 +91,6 @@ function add_proxy_response( $which, $parent_path ) { $collection->description = ''; $resource = new DAVResource($collection); - $resource->FetchPrincipal(); return $resource->RenderAsXML($property_list, $reply); } diff --git a/inc/caldav-PUT-functions.php b/inc/caldav-PUT-functions.php index bd7a6fa5..24b9e30a 100644 --- a/inc/caldav-PUT-functions.php +++ b/inc/caldav-PUT-functions.php @@ -16,7 +16,8 @@ */ require_once('AwlCache.php'); -require_once('iCalendar.php'); +require_once('vComponent.php'); +require_once('vCalendar.php'); require_once('WritableCollection.php'); $bad_events = null; @@ -159,10 +160,23 @@ function public_events_only( $user_no, $dav_name ) { return false; } +/** + * Get a TZID string from this VEVENT/VTODO/... component if we can + * @param vComponent $comp + * @return The TZID value we found, or null + */ +function GetTZID( vComponent $comp ) { + $p = $comp->GetProperty('DTSTART'); + if ( !isset($p) && $comp->GetType() == 'VTODO' ) { + $p = $comp->GetProperty('DUE'); + } + if ( !isset($p) ) return null; + return $p->GetParameterValue('TZID'); +} /** * Deliver scheduling requests to attendees -* @param iCalComponent $ical the VCALENDAR to deliver +* @param vComponent $ical the VCALENDAR to deliver */ function handle_schedule_request( $ical ) { global $c, $session, $request; @@ -207,7 +221,7 @@ function handle_schedule_request( $ical ) { $attendee->SetParameterValue ('SCHEDULE-STATUS','5.3;No scheduling support for user'); continue; } - $deliver_path = $attendee_principal->internal_url('schedule_inbox'); + $deliver_path = $attendee_principal->internal_url('schedule-inbox'); $ar = new DAVResource($deliver_path); $priv = $ar->HavePrivilegeTo('schedule-deliver-invite' ); @@ -223,29 +237,23 @@ function handle_schedule_request( $ical ) { $attendee->SetParameterValue ('SCHEDULE-STATUS','1.2;Scheduling message has been delivered'); - $ncal = new iCalComponent ( ); - $ncal->VCalendar (); - $ncal->AddProperty ( 'METHOD', 'REQUEST' ); + $ncal = new vCalendar( array('METHOD' => 'REQUEST') ); $ncal->AddComponent ( array_merge ( $ical->GetComponents('VEVENT',false) , array ($ic) )); $content = $ncal->Render(); $cid = $ar->GetProperty('collection_id'); dbg_error_log('DELIVER', 'to user: %s, to path: %s, collection: %s, from user: %s, caldata %s', $attendee_principal->user_no(), $deliver_path, $cid, $request->user_no, $content ); - write_resource( $attendee_principal->user_no(), $deliver_path . $etag . '.ics' , - $content , $ar->GetProperty('collection_id'), $request->user_no, - md5($content), $ncal, $put_action_type='INSERT', $caldav_context=true, $log_action=true, $etag ); - $attendee->SetParameterValue ('SCHEDULE-STATUS','1.2;Scheduling message has been delivered'); + write_resource( $deliver_path . $etag . '.ics', $content, $ar, $request->user_no, + md5($content), $ncal, $put_action_type='INSERT', $caldav_context=true, $log_action=true, $etag ); + $attendee->SetParameterValue ('SCHEDULE-STATUS','1.2;Scheduling message has been delivered'); } // don't write an entry in the out box, ical doesn't delete it or ever read it again - $ncal = new iCalComponent ( ); - $ncal->VCalendar (); - $ncal->AddProperty ( 'METHOD', 'REQUEST' ); + $ncal = new vCalendar(array('METHOD' => 'REQUEST')); $ncal->AddComponent ( array_merge ( $ical->GetComponents('VEVENT',false) , array ($ic) )); $content = $ncal->Render(); - $deliver_path = $request->principal->internal_url('schedule_inbox'); + $deliver_path = $request->principal->internal_url('schedule-inbox'); $ar = new DAVResource($deliver_path); - write_resource( $request->user_no, $deliver_path . $etag . '.ics' , - $content , $ar->GetProperty('collection_id'), $request->user_no, - md5($content), $ncal, $put_action_type='INSERT', $caldav_context=true, $log_action=true, $etag ); + write_resource( $deliver_path . $etag . '.ics', $content, $ar, $request->user_no, md5($content), + $ncal, $put_action_type='INSERT', $caldav_context=true, $log_action=true, $etag ); //$etag = md5($content); header('ETag: "'. $etag . '"' ); header('Schedule-Tag: "'.$etag . '"' ); @@ -254,10 +262,10 @@ function handle_schedule_request( $ical ) { /** * Deliver scheduling replies to organizer and other attendees -* @param iCalComponent $ical the VCALENDAR to deliver +* @param vComponent $ical the VCALENDAR to deliver * @return false on error */ -function handle_schedule_reply ( $ical ) { +function handle_schedule_reply ( vComponent $ical ) { global $c, $session, $request; $resources = $ical->GetComponents('VTIMEZONE',false); $ic = $resources[0]; @@ -298,14 +306,11 @@ function handle_schedule_reply ( $ical ) { continue; } - $ncal = new iCalComponent ( ); - $ncal->VCalendar (); - $ncal->AddProperty ( 'METHOD', 'REPLY' ); + $ncal = new vCalendar( array('METHOD' => 'REPLY') ); $ncal->AddComponent ( array_merge ( $ical->GetComponents('VEVENT',false) , array ($ic) )); $content = $ncal->Render(); - write_resource( $attendee_principal->user_no(), $deliver_path . $etag . '.ics' , - $content , $ar->GetProperty('collection_id'), $request->user_no, - md5($content), $ncal, $put_action_type='INSERT', $caldav_context=true, $log_action=true, $etag ); + write_resource( $deliver_path . $etag . '.ics', $content, $ar, $request->user_no, md5($content), + $ncal, $put_action_type='INSERT', $caldav_context=true, $log_action=true, $etag ); } $request->DoResponse( 201, 'Created' ); } @@ -315,23 +320,23 @@ function handle_schedule_reply ( $ical ) { /** * Create a scheduling request in the schedule inbox for the -* @param iCalComponent $resource The VEVENT/VTODO/... resource we are scheduling -* @param iCalProp $attendee The attendee we are scheduling +* @param vComponent $resource The VEVENT/VTODO/... resource we are scheduling +* @param vProperty $attendee The attendee we are scheduling * @return float The result of the scheduling request, per caldav-sched #3.5.4 */ -function write_scheduling_request( &$resource, $attendee_value, $create_resource ) { - $email = preg_replace( '/^mailto:/', '', $attendee_value ); +function write_scheduling_request( vComponent $resource, $attendee_value, $create_resource ) { + $email = preg_replace( '/^mailto:/i', '', $attendee_value ); $schedule_target = new Principal('email',$email); if ( $schedule_target->Exists() ) { $attendee_inbox = new WritableCollection(array('path' => $schedule_target->internal_url('schedule-inbox'))); if ( ! $attendee_inbox->HavePrivilegeTo('schedule-deliver-invite') ) { $response = '3.8;'.translate('No authority to deliver invitations to user.'); } - else if ( $attendee_inbox->WriteCalendarMember($resource, $create_resource) ) { - $response = '2.0;'.translate('Scheduling invitation delivered successfully'); + else if ( $attendee_inbox->WriteCalendarMember($resource, $create_resource) === false ) { + $response = '5.3;'.translate('No scheduling support for user'); } else { - $response = '5.3;'.translate('No scheduling support for user'); + $response = '2.0;'.translate('Scheduling invitation delivered successfully'); } } else { @@ -342,9 +347,9 @@ function write_scheduling_request( &$resource, $attendee_value, $create_resource /** * Create scheduling requests in the schedule inbox for the -* @param iCalComponent $resource The VEVENT/VTODO/... resource we are scheduling +* @param vComponent $resource The VEVENT/VTODO/... resource we are scheduling */ -function create_scheduling_requests( &$resource ) { +function create_scheduling_requests( vComponent $resource ) { if ( ! is_object($resource) ) { dbg_error_log( 'PUT', 'create_scheduling_requests called with non-object parameter (%s)', gettype($resource) ); return; @@ -365,16 +370,18 @@ function create_scheduling_requests( &$resource ) { dbg_error_log( 'PUT', 'Adding to scheduling inbox %d attendees', count($attendees) ); foreach( $attendees AS $attendee ) { - $attendee->SetParameterValue( 'SCHEDULE-STATUS', write_scheduling_request( $resource, $attendee->Value(), true ) ); + $schedule_status = write_scheduling_request( $resource, $attendee->Value(), true ); + dbg_error_log( 'PUT', 'Status for attendee <%s> set to "%s"', $attendee->Value(), $schedule_status ); + $attendee->SetParameterValue( 'SCHEDULE-STATUS', $schedule_status ); } } /** * Update scheduling requests in the schedule inbox for the -* @param iCalComponent $resource The VEVENT/VTODO/... resource we are scheduling +* @param vComponent $resource The VEVENT/VTODO/... resource we are scheduling */ -function update_scheduling_requests( &$resource ) { +function update_scheduling_requests( vComponent $resource ) { if ( ! is_object($resource) ) { dbg_error_log( 'PUT', 'update_scheduling_requests called with non-object parameter (%s)', gettype($resource) ); return; @@ -420,7 +427,7 @@ function import_collection( $ics_content, $user_no, $path, $caldav_context, $app } } - $calendar = new iCalComponent($ics_content); + $calendar = new vComponent($ics_content); $timezones = $calendar->GetComponents('VTIMEZONE',true); $components = $calendar->GetComponents('VTIMEZONE',false); @@ -436,7 +443,7 @@ function import_collection( $ics_content, $user_no, $path, $caldav_context, $app $tz_ids[$tz->GetPValue('TZID')] = $k; } - /** Build an array of resources. Each resource is an array of iCalComponent */ + /** Build an array of resources. Each resource is an array of vComponent */ $resources = array(); foreach( $components AS $k => $comp ) { $uid = $comp->GetPValue('UID'); @@ -449,9 +456,8 @@ function import_collection( $ics_content, $user_no, $path, $caldav_context, $app $resources[$uid][] = $comp; /** Ensure we have the timezone component for this in our array as well */ - $tzid = $comp->GetPParamValue('DTSTART', 'TZID'); - if ( !isset($tzid) || $tzid == '' ) $tzid = $comp->GetPParamValue('DUE','TZID'); - if ( !isset($resources[$uid][$tzid]) && isset($tz_ids[$tzid]) ) { + $tzid = GetTZID($comp); + if ( !empty($tzid) && !isset($resources[$uid][$tzid]) && isset($tz_ids[$tzid]) ) { $resources[$uid][$tzid] = $timezones[$tz_ids[$tzid]]; } } @@ -491,8 +497,7 @@ EOSQL; if ( isset($c->skip_bad_event_on_import) && $c->skip_bad_event_on_import ) $qry->Begin(); /** Construct the VCALENDAR data */ - $vcal = new iCalComponent(); - $vcal->VCalendar(); + $vcal = new vCalendar(); $vcal->SetComponents($resource); create_scheduling_requests($vcal); $icalendar = $vcal->Render(); @@ -549,7 +554,8 @@ EOSQL; * So we're looking for 'VALUE=DATE', to identify the duration, effectively. * */ - $value_type = $first->GetPParamValue('DTSTART','VALUE'); + $dtstart_prop = $first->GetProperty('DTSTART'); + $value_type = $dtstart_prop->GetParameterValue('VALUE'); dbg_error_log('PUT','DTSTART without DTEND. DTSTART value type is %s', $value_type ); if ( isset($value_type) && $value_type == 'DATE' ) $dtend = '(:dtstart::timestamp with time zone::date + \'1 day\'::interval)'; @@ -563,7 +569,7 @@ EOSQL; $calitem_params[':modified'] = $last_modified; $dtstamp = $first->GetPValue('DTSTAMP'); - if ( !isset($dtstamp) || $dtstamp == '' ) $dtstamp = $last_modified; + if ( empty($dtstamp) ) $dtstamp = $last_modified; $calitem_params[':dtstamp'] = $dtstamp; /** RFC2445, 4.8.1.3: Default is PUBLIC, or also if overridden by the collection settings */ @@ -573,9 +579,8 @@ EOSQL; /** Calculate what timezone to set, first, if possible */ - $tzid = $first->GetPParamValue('DTSTART','TZID'); - if ( !isset($tzid) || $tzid == '' ) $tzid = $first->GetPParamValue('DUE','TZID'); - if ( isset($tzid) && $tzid != '' ) { + $tzid = GetTZID($first); + if ( !empty($tzid) ) { if ( isset($resource[$tzid]) ) { $tz = $resource[$tzid]; $tz_locn = $tz->GetPValue('X-LIC-LOCATION'); @@ -649,14 +654,14 @@ EOSQL; /** -* Given a dav_id and an original iCalComponent, pull out each of the VALARMs +* Given a dav_id and an original vComponent, pull out each of the VALARMs * and write the values into the calendar_alarm table. * * @param int $dav_id The dav_id of the caldav_data we're processing -* @param iCalComponent The VEVENT or VTODO containing the VALARM +* @param vComponent The VEVENT or VTODO containing the VALARM * @return null */ -function write_alarms( $dav_id, $ical ) { +function write_alarms( $dav_id, vComponent $ical ) { $qry = new AwlQuery('DELETE FROM calendar_alarm WHERE dav_id = '.$dav_id ); $qry->Exec('PUT',__LINE__,__FILE__); @@ -710,10 +715,10 @@ function write_alarms( $dav_id, $ical ) { * Parse out the attendee property and write a row to the * calendar_attendee table for each one. * @param int $dav_id The dav_id of the caldav_data we're processing -* @param iCalComponent The VEVENT or VTODO containing the ATTENDEEs +* @param vComponent The VEVENT or VTODO containing the ATTENDEEs * @return null */ -function write_attendees( $dav_id, $ical ) { +function write_attendees( $dav_id, vComponent $ical ) { $qry = new AwlQuery('DELETE FROM calendar_attendee WHERE dav_id = '.$dav_id ); $qry->Exec('PUT',__LINE__,__FILE__); @@ -754,7 +759,7 @@ function write_attendees( $dav_id, $ical ) { * @param int $collection_id The ID of the collection containing the resource being written * @param int $author The user_no who wants to put this resource on the server * @param string $etag An etag unique for this event -* @param object $ic The parsed iCalendar object +* @param vComponent $ic The parsed iCalendar object * @param string $put_action_type INSERT or UPDATE depending on what we are to do * @param boolean $caldav_context True, if we are responding via CalDAV, false for other ways of calling this * @param string Either 'INSERT' or 'UPDATE': the type of action we are doing @@ -762,7 +767,8 @@ function write_attendees( $dav_id, $ical ) { * @param string $weak_etag An etag that is NOT modified on ATTENDEE changes for this event * @return boolean True for success, false for failure. */ -function write_resource( $user_no, $path, $caldav_data, $collection_id, $author, $etag, $ic, $put_action_type, $caldav_context, $log_action=true, $weak_etag=null ) { +function write_resource( $path, $caldav_data, DAVResource $collection, $author, $etag, + vComponent $ic, $put_action_type, $caldav_context, $log_action=true, $weak_etag=null ) { global $tz_regex; $resources = $ic->GetComponents('VTIMEZONE',false); // Not matching VTIMEZONE @@ -774,8 +780,15 @@ function write_resource( $user_no, $path, $caldav_data, $collection_id, $author, } else { $first = $resources[0]; + if ( !($first instanceof vComponent) ) { + print $ic->Render(); + fatal('This is not a vComponent!'); + } $resource_type = $first->GetType(); } + + $user_no = $collection->user_no(); + $collection_id = $collection->collection_id(); $qry = new AwlQuery(); $qry->Begin(); @@ -806,13 +819,14 @@ function write_resource( $user_no, $path, $caldav_data, $collection_id, $author, $dav_id = $row->dav_id; $dav_params[':dav_id'] = $dav_id; $calitem_params[':dav_id'] = $dav_id; - - $dtstart = $first->GetPValue('DTSTART'); - $calitem_params[':dtstart'] = $dtstart; - if ( (!isset($dtstart) || $dtstart == '') && $first->GetPValue('DUE') != '' ) { - $dtstart = $first->GetPValue('DUE'); - } + $due = null; + if ( $first->GetType() == 'VTODO' ) $due = $first->GetPValue('DUE'); + $calitem_params[':due'] = $due; + $dtstart = $first->GetPValue('DTSTART'); + if ( empty($dtstart) ) $dtstart = $due; + $calitem_params[':dtstart'] = $dtstart; + $dtend = $first->GetPValue('DTEND'); if ( isset($dtend) && $dtend != '' ) { dbg_error_log( 'PUT', ' DTEND: "%s", DTSTART: "%s", DURATION: "%s"', $dtend, $dtstart, $first->GetPValue('DURATION') ); @@ -820,6 +834,7 @@ function write_resource( $user_no, $path, $caldav_data, $collection_id, $author, $dtend = ':dtend'; } else { + // In this case we'll construct the SQL directly as a calculation relative to :dtstart $dtend = 'NULL'; if ( $first->GetPValue('DURATION') != '' AND $dtstart != '' ) { $duration = trim(preg_replace( '#[PT]#', ' ', $first->GetPValue('DURATION') )); @@ -841,7 +856,8 @@ function write_resource( $user_no, $path, $caldav_data, $collection_id, $author, * So we're looking for 'VALUE=DATE', to identify the duration, effectively. * */ - $value_type = $first->GetPParamValue('DTSTART','VALUE'); + $dtstart_prop = $first->GetProperty('DTSTART'); + $value_type = $dtstart_prop->GetParameterValue('VALUE'); dbg_error_log('PUT','DTSTART without DTEND. DTSTART value type is %s', $value_type ); if ( isset($value_type) && $value_type == 'DATE' ) $dtend = '(:dtstart::timestamp with time zone::date + \'1 day\'::interval)'; @@ -882,20 +898,17 @@ function write_resource( $user_no, $path, $caldav_data, $collection_id, $author, } $calitem_params[':class'] = $class; - /** Calculate what timezone to set, first, if possible */ $last_tz_locn = 'Turkmenikikamukau'; // I really hope this location doesn't exist! - $tzid = $first->GetPParamValue('DTSTART','TZID'); - if ( !isset($tzid) || $tzid == '' ) $tzid = $first->GetPParamValue('DUE','TZID'); + $tzid = GetTZID($first); $timezones = $ic->GetComponents('VTIMEZONE'); foreach( $timezones AS $k => $tz ) { if ( $tz->GetPValue('TZID') != $tzid ) { /** - * We'll pretend they didn't forget to give us a TZID and that they - * really hope the server is running in the timezone they supplied... but be noisy about it. + * We'll skip any tz definitions that are for a TZID other than the DTSTART/DUE on the first VEVENT/VTODO */ - dbg_error_log( 'ERROR', ' Event includes TZID[%s] but uses TZID[%s]!', $tz->GetPValue('TZID'), $tzid ); - $tzid = $tz->GetPValue('TZID'); + dbg_error_log( 'ERROR', ' Event uses TZID[%s], skipping included TZID[%s]!', $tz->GetPValue('TZID'), $tzid ); + continue; } // This is the one $tz_locn = $tz->GetPValue('X-LIC-LOCATION'); @@ -941,13 +954,12 @@ function write_resource( $user_no, $path, $caldav_data, $collection_id, $author, $calitem_params[':rrule'] = $first->GetPValue('RRULE'); $calitem_params[':url'] = $first->GetPValue('URL'); $calitem_params[':priority'] = $first->GetPValue('PRIORITY'); - $calitem_params[':due'] = $first->GetPValue('DUE'); $calitem_params[':percent_complete'] = $first->GetPValue('PERCENT-COMPLETE'); $calitem_params[':status'] = $first->GetPValue('STATUS'); if ( !isset($dav_params[':modified']) ) $dav_params[':modified'] = 'now'; if ( $put_action_type == 'INSERT' ) { - create_scheduling_requests($vcal); + if ( !$collection->IsSchedulingCollection() ) create_scheduling_requests($ic); $sql = 'INSERT INTO caldav_data ( dav_id, user_no, dav_name, dav_etag, caldav_data, caldav_type, logged_user, created, modified, collection_id, weak_etag ) VALUES( :dav_id, :user_no, :dav_name, :etag, :dav_data, :caldav_type, :session_user, :created, :modified, :collection_id, :weak_etag )'; $dav_params[':collection_id'] = $collection_id; @@ -956,11 +968,12 @@ function write_resource( $user_no, $path, $caldav_data, $collection_id, $author, $dav_params[':created'] = (isset($created) && $created != '' ? $created : $dtstamp); } else { - update_scheduling_requests($vcal); + if ( !$collection->IsSchedulingCollection() ) update_scheduling_requests($ic); $sql = 'UPDATE caldav_data SET caldav_data=:dav_data, dav_etag=:etag, caldav_type=:caldav_type, logged_user=:session_user, modified=:modified, weak_etag=:weak_etag WHERE dav_id=:dav_id'; } if ( !$qry->QDo($sql,$dav_params) ) { + fatal('Insert into calendar_item failed...'); rollback_on_error( $caldav_context, $user_no, $path); return false; } @@ -972,7 +985,7 @@ INSERT INTO calendar_item (user_no, dav_name, dav_id, dav_etag, uid, dtstamp, dtstart, dtend, summary, location, class, transp, description, rrule, tz_id, last_modified, url, priority, created, due, percent_complete, status, collection_id ) - VALUES ( :user_no, :dav_name, currval('dav_id_seq'), :etag, :uid, :dtstamp, + VALUES ( :user_no, :dav_name, :dav_id, :etag, :uid, :dtstamp, :dtstart, $dtend, :summary, :location, :class, :transp, :description, :rrule, :tzid, :modified, :url, :priority, :created, :due, :percent_complete, :status, :collection_id ) @@ -986,8 +999,9 @@ EOSQL; else { $sql = << $collection_path ) ); - if ( $qry->Exec('PUT',__LINE__,__FILE__) && $qry->rows() == 1 ) { - $collection = $qry->Fetch(); - $user_no = $collection->user_no; + $collection = new DAVResource($collection_path); + if ( $collection->IsCollection() || $collection->IsSchedulingCollection() ) { - return write_resource( $user_no, $path, $caldav_data, $collection->collection_id, $user_no, $etag, $ic, $put_action_type, false, $write_action_log ); + $vc = new vComponent( $caldav_data ); + return write_resource( $path, $caldav_data, $collection, $user_no, $etag, $vc, $put_action_type, false, $write_action_log ); } return false; } diff --git a/inc/caldav-PUT.php b/inc/caldav-PUT.php index 3f783b68..35a8c648 100644 --- a/inc/caldav-PUT.php +++ b/inc/caldav-PUT.php @@ -53,7 +53,7 @@ if ( $dav_resource->IsCollection() ) { } $etag = md5($request->raw_post); -$ic = new iCalComponent( $request->raw_post ); +$ic = new vComponent( $request->raw_post ); if ( ! $dav_resource->Exists() && (isset($request->etag_if_match) && $request->etag_if_match != '') ) { /** @@ -90,8 +90,9 @@ if ( $dav_resource->Exists() ) { } $put_action_type = ($dav_resource->Exists() ? 'UPDATE' : 'INSERT'); +$collection = $dav_resource->GetParentContainer(); -write_resource( $dav_resource->GetProperty('user_no'), $dav_resource->bound_from(), $request->raw_post, $dav_resource->GetProperty('collection_id'), +write_resource( $dav_resource->bound_from(), $request->raw_post, $collection, $session->user_no, $etag, $ic, $put_action_type, true, true ); header(sprintf('ETag: "%s"', $etag) ); diff --git a/testing/tests/scheduling/3010-PUT-with-attendees.result b/testing/tests/scheduling/3010-PUT-with-attendees.result new file mode 100644 index 00000000..8f34da77 --- /dev/null +++ b/testing/tests/scheduling/3010-PUT-with-attendees.result @@ -0,0 +1,71 @@ +HTTP/1.1 100 Continue + +HTTP/1.1 204 No Content +Date: Dow, 01 Jan 2000 00:00:00 GMT +DAV: 1, 2, 3, access-control, calendar-access, calendar-schedule +DAV: extended-mkcol, calendar-proxy, bind, addressbook, calendar-auto-schedule +ETag: "e69b6d5b70189b96126c8993bcf1648a" +Content-Length: 0 +Content-Type: text/plain; charset="utf-8" + + + caldav_type: >VEVENT< + dav_etag: >e69b6d5b70189b96126c8993bcf1648a< + dtend: >2010-11-23 16:00:00< + dtstamp: >2010-11-24 22:19:00< + dtstart: >2010-11-23 11:00:00< + due: >NULL< + location: >NULL< + logged_user: >10< + summary: >Multiple Beard Meeting< + uid: >PUT-attendees< + user_no: >10< + + caldav_type: >VEVENT< + dav_etag: >e69b6d5b70189b96126c8993bcf1648a< + dtend: >2010-11-23 16:00:00< + dtstamp: >2010-11-24 22:19:00< + dtstart: >2010-11-23 11:00:00< + due: >NULL< + location: >NULL< + logged_user: >10< + summary: >Multiple Beard Meeting< + uid: >PUT-attendees< + user_no: >11< + + caldav_type: >VEVENT< + dav_etag: >e69b6d5b70189b96126c8993bcf1648a< + dtend: >2010-11-23 16:00:00< + dtstamp: >2010-11-24 22:19:00< + dtstart: >2010-11-23 11:00:00< + due: >NULL< + location: >NULL< + logged_user: >10< + summary: >Multiple Beard Meeting< + uid: >PUT-attendees< + user_no: >12< + + caldav_type: >VEVENT< + dav_etag: >e69b6d5b70189b96126c8993bcf1648a< + dtend: >2010-11-23 16:00:00< + dtstamp: >2010-11-24 22:19:00< + dtstart: >2010-11-23 11:00:00< + due: >NULL< + location: >NULL< + logged_user: >10< + summary: >Multiple Beard Meeting< + uid: >PUT-attendees< + user_no: >13< + + caldav_type: >VEVENT< + dav_etag: >e69b6d5b70189b96126c8993bcf1648a< + dtend: >2010-11-23 16:00:00< + dtstamp: >2010-11-24 22:19:00< + dtstart: >2010-11-23 11:00:00< + due: >NULL< + location: >NULL< + logged_user: >10< + summary: >Multiple Beard Meeting< + uid: >PUT-attendees< + user_no: >101< + diff --git a/testing/tests/scheduling/3010-PUT-with-attendees.test b/testing/tests/scheduling/3010-PUT-with-attendees.test new file mode 100644 index 00000000..8ebab2b0 --- /dev/null +++ b/testing/tests/scheduling/3010-PUT-with-attendees.test @@ -0,0 +1,74 @@ +# +# PUT an event with attendees +# +TYPE=PUT +URL=http://mycaldav/caldav.php/resource2/home/PUT-attendees.ics +HEADER=Content-Type: text/calendar +HEADER=User-Agent: DAVKit/2.0 (10.5.1; wrbt) iCal 3.0.1 +HEAD + + +BEGINDATA +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Apple Inc.//iCal 3.0//EN +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Pacific/Auckland +BEGIN:STANDARD +TZOFFSETFROM:+1300 +TZOFFSETTO:+1200 +DTSTART:19900318T030000 +RRULE:FREQ=YEARLY;UNTIL=20070317T140000Z;BYMONTH=3;BYDAY=3SU +TZNAME:NZST +END:STANDARD +BEGIN:DAYLIGHT +TZOFFSETFROM:+1200 +TZOFFSETTO:+1300 +DTSTART:20070930T020000 +RRULE:FREQ=YEARLY;BYMONTH=9;BYDAY=-1SU +TZNAME:NZDT +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:+1300 +TZOFFSETTO:+1200 +DTSTART:20080406T030000 +RRULE:FREQ=YEARLY;BYMONTH=4;BYDAY=1SU +TZNAME:NZST +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +SEQUENCE:7 +DESCRIPTION:Masses of Lunch will be needed +UID:PUT-attendees +TRANSP:OPAQUE +DTSTART;TZID=Pacific/Auckland:20101123T110000 +DTEND;TZID=Pacific/Auckland:20101123T160000 +DTSTAMP:20101124T221900Z +SUMMARY:Multiple Beard Meeting +CREATED:20071124T221521Z +RRULE:FREQ=MONTHLY;INTERVAL=1;BYDAY=4TU +ORGANIZER;CN=User 1:MAILTO:user1@example.net +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION; + RSVP=TRUE;CN=User 1;LANGUAGE=en:MAILTO:user1@example.net +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION; + RSVP=TRUE;CN=User 2;LANGUAGE=en:MAILTO:user2@example.net +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION; + RSVP=TRUE;CN=User 3;LANGUAGE=en:MAILTO:user3@example.net +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION; + RSVP=TRUE;CN=User 4;LANGUAGE=en:MAILTO:user4@example.net +END:VEVENT +END:VCALENDAR +ENDDATA + + +QUERY +SELECT caldav_data.user_no, caldav_data.dav_etag, caldav_type, logged_user, + uid, dtstamp, + dtstart at time zone tz_locn as dtstart, + dtend at time zone tz_locn as dtend, + due, summary, location +FROM caldav_data JOIN calendar_item USING(dav_name) LEFT JOIN time_zone USING (tz_id) +WHERE calendar_item.uid = 'PUT-attendees'; +ENDQUERY +