diff --git a/docs/website/clients/Chandler-details.php b/docs/website/clients/Chandler-details.php index ba301955..8214309f 100644 --- a/docs/website/clients/Chandler-details.php +++ b/docs/website/clients/Chandler-details.php @@ -7,7 +7,7 @@ written in Python.
caldav://server.domain.name/caldav.php/username/home/, enter your user name for DAViCal and click "OK".
caldav://server.domain.name/caldav.php/username/calendar/, enter your user name for DAViCal and click "OK".
The host name is, of course, up to you. The 'root path' should be /caldav.php/ and anything following that is the calendar namespace.
Within the calendar namespace DAViCal uses the first element of the path as the user or resource name, so that a client connecting at the root path -can see all of the (accessible) users and resources available to them (Mulberry displays this hierarchy) with any calendars below that.
-Effectively this means that in Evolution, Sunbird and Lightning you should really specify a calendar URL which is something like:
+The host name is, of course, up to you. The 'root path' should be
+/caldav.php/ and anything following that is the calendar
+namespace.
Within the calendar namespace DAViCal uses the first element of the +path as the user or 'princpal' name, so that a client connecting at the +root path can see all of the (accessible) users and resources available +to them (Mulberry displays this hierarchy) with any calendars below that.
+ +This means that in Evolution, Lightning and other software wanting a +'calendar' URL you should specify a URL which is something like:
-http://calendar.example.net/caldav.php/username/home/ +http://calendar.example.net/caldav.php/username/calendar/-
Then, when more calendar client software sees it as useful to be able to browse that hierarchy, you won't be up for any heavy database manipulation.
-I may well enforce this standard in some way before release 1.0, as well as auto-creating the collection records when Evolution, Lightning
-or Sunbird attempt to store to a non-existent collection.
DAViCal creates two collections automatically when a user is created. In +recent versions these are called 'calendar' and 'addressbook'. Some software +also makes it easy to create more calendars and addressbooks, or you can create +more through DAViCal's web interface, also.
+ +In older versions of DAViCal (pre 0.9.9.5) the default calendar was named 'home' +and there was no default addressbook.
diff --git a/docs/website/clients/Mozilla-details.php b/docs/website/clients/Mozilla-details.php index 9a637fd9..99cbdc7c 100644 --- a/docs/website/clients/Mozilla-details.php +++ b/docs/website/clients/Mozilla-details.php @@ -8,7 +8,7 @@
If you want to point me at more free software that supports CalDAV, or send me free copies of such proprietary software, then I will add it to the list as well as make DAViCal work with it.
+ +In the general CalDAV terminology, client software will want to know +several facts about the CalDAV server. Some (like iCal and iOS) will try +and discover these facts for themselves, and others (like Lightning and +Evolution) will require you to enter some information. When they ask for +that information they will be asking for the following things:
+Typically the answers, in DAViCal's case, are:
+There could well be a wider range of information about many and varied client + software on the DAViCal Wiki as well. \ No newline at end of file diff --git a/htdocs/admin.php b/htdocs/admin.php index f2117974..7fe272c3 100644 --- a/htdocs/admin.php +++ b/htdocs/admin.php @@ -24,7 +24,7 @@ if ( ! @include_once( $code_file ) ) { $c->messages[] = sprintf('No page found to %s %s%s%s', $action, ($action == 'browse' ? '' : 'a '), $component, ($action == 'browse' ? 's' : '')); include('page-header.php'); include('page-footer.php'); - exit(0); + @ob_flush(); exit(0); } include('page-header.php'); diff --git a/htdocs/always.php b/htdocs/always.php index f84bd95d..256f0e6d 100644 --- a/htdocs/always.php +++ b/htdocs/always.php @@ -26,6 +26,7 @@ function early_exception_handler($e) { foreach( $trace AS $k => $v ) { 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']:'') ); } + @ob_flush(); } set_exception_handler('early_exception_handler'); @@ -89,7 +90,7 @@ if ( ! @include_once('AWLUtilities.php') ) { } if ( ! @include_once('AWLUtilities.php') ) { echo "Could not find the AWL libraries. Are they installed? Check your include_path in php.ini!\n"; - exit; + @ob_flush(); exit(0); } } @@ -145,7 +146,7 @@ else if ( @file_exists('config/config.php') ) { } else { include('davical_configuration_missing.php'); - exit; + @ob_flush(); exit(0); } $config_warnings = trim(ob_get_contents()); ob_end_clean(); diff --git a/htdocs/caldav.php b/htdocs/caldav.php index 8b0d9334..97845e0f 100644 --- a/htdocs/caldav.php +++ b/htdocs/caldav.php @@ -17,17 +17,17 @@ if ( isset($_SERVER['PATH_INFO']) && preg_match( '{^(/favicon.ico|davical.css|(i else { fpassthru($fh); } - exit(0); + @ob_flush(); exit(0); } require_once('./always.php'); if ( isset($_SERVER['PATH_INFO']) && preg_match( '{^/\.well-known/(.+)$}', $_SERVER['PATH_INFO'], $matches ) ) { require ('well-known.php'); - exit(0); + @ob_flush(); exit(0); } elseif ( isset($_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO'] == '/autodiscover/autodiscover.xml' ) { require ('autodiscover-handler.php'); - exit(0); + @ob_flush(); exit(0); } function logRequestHeaders() { @@ -102,7 +102,7 @@ if ( ! ($request->IsPrincipal() || isset($request->collection) || $request->meth $redirect_url = ConstructURL('/caldav.php'.$matches[1]); dbg_error_log( 'LOG WARNING', 'Redirecting %s for "%s" to "%s"', $request->method, $request->path, $redirect_url ); header('Location: '.$redirect_url ); - exit(0); + @ob_flush(); exit(0); } } diff --git a/htdocs/index.php b/htdocs/index.php index cdbc96c9..782578e1 100644 --- a/htdocs/index.php +++ b/htdocs/index.php @@ -4,7 +4,7 @@ if ( $_SERVER['REQUEST_METHOD'] != "GET" && $_SERVER['REQUEST_METHOD'] != "POST" * If the request is not a GET or POST then they must really want caldav.php! */ include("./caldav.php"); - exit; // Not that it should return from that! + @ob_flush(); exit(0); // Not that it should return from that! } include("./always.php"); diff --git a/htdocs/tools.php b/htdocs/tools.php index 6e60b978..e73a31ea 100644 --- a/htdocs/tools.php +++ b/htdocs/tools.php @@ -21,7 +21,7 @@ require_once("caldav-PUT-functions.php"); include_once('check_UTF8.php'); if ( !$session->AllowedTo("Admin" ) ) - exit; + @ob_flush(); exit(0); if( function_exists("sync_LDAP") && isset($_POST['Sync_LDAP'])){ sync_LDAP(); diff --git a/inc/CalDAVRequest.php b/inc/CalDAVRequest.php index 4455472e..52649da3 100644 --- a/inc/CalDAVRequest.php +++ b/inc/CalDAVRequest.php @@ -1201,8 +1201,7 @@ EOSQL; @dbg_error_log("statistics", "Method: %s, Status: %d, Script: %5.3lfs, Queries: %5.3lfs, URL: %s", $this->method, $status, $script_time, $c->total_query_time, $this->path); } - - exit(0); + @ob_flush(); exit(0); } } diff --git a/inc/DAVResource.php b/inc/DAVResource.php index 486ed5f1..7cb3f5b2 100644 --- a/inc/DAVResource.php +++ b/inc/DAVResource.php @@ -283,6 +283,17 @@ class DAVResource $this->resource->location = null; $this->resource->url = null; } + else if ( isset($c->hide_alarm) && $c->hide_alarm && !$this->HavePrivilegeTo('write') ) { + $vcal1 = new iCalComponent($this->resource->caldav_data); + $comps = $vcal1->GetComponents(); + $vcal2 = new iCalComponent(); + $vcal2->VCalendar(); + foreach( $comps AS $comp ) { + $comp->ClearComponents('VALARM'); + $vcal2->AddComponent($comp); + } + $this->resource->caldav_data = $vcal2->Render(); + } } else if ( strtoupper(substr($this->resource->caldav_data,0,11)) == 'BEGIN:VCARD' ) { $this->contenttype = 'text/vcard'; @@ -1048,8 +1059,8 @@ EOQRY; /** - * Checks whether this resource is a calendar - * @param string $type The type of scheduling collection, 'read', 'write' or 'any' + * Checks whether this resource is a scheduling inbox/outbox collection + * @param string $type The type of scheduling collection, 'inbox', 'outbox' or 'any' */ function IsSchedulingCollection( $type = 'any' ) { if ( $this->_is_collection && preg_match( '{schedule-(inbox|outbox)}', $this->collection->type, $matches ) ) { @@ -1059,6 +1070,18 @@ EOQRY; } + /** + * Checks whether this resource is IN a scheduling inbox/outbox collection + * @param string $type The type of scheduling collection, 'inbox', 'outbox' or 'any' + */ + function IsInSchedulingCollection( $type = 'any' ) { + if ( !$this->_is_collection && preg_match( '{schedule-(inbox|outbox)}', $this->collection->type, $matches ) ) { + return ($type == 'any' || $type == $matches[1]); + } + return false; + } + + /** * Checks whether this resource is an addressbook */ @@ -1245,6 +1268,14 @@ EOQRY; } + /** + * Checks whether the target collection is for public events only + */ + function IsPublicOnly() { + return ( isset($this->collection->publicly_events_only) && $this->collection->publicly_events_only == 't' ); + } + + /** * Return the type of whatever contains this resource, or would if it existed. */ @@ -1371,6 +1402,11 @@ EOQRY; return clone($this->resource); break; + case 'dav-data': + if ( !isset($this->resource) ) $this->FetchResource(); + return $this->resource->caldav_data; + break; + case 'principal': if ( !isset($this->principal) ) $this->FetchPrincipal(); return clone($this->principal); diff --git a/inc/DAViCalSession.php b/inc/DAViCalSession.php index e06f1384..68d0f97a 100644 --- a/inc/DAViCalSession.php +++ b/inc/DAViCalSession.php @@ -113,7 +113,7 @@ class DAViCalSession extends Session || (isset($c->restrict_admin_port) && $c->restrict_admin_port != $_SERVER['SERVER_PORT'] ) ) { header('Location: caldav.php'); dbg_error_log( 'LOG WARNING', 'Access to "%s" via "%s:%d" rejected.', $_SERVER['REQUEST_URI'], $current_domain, $_SERVER['SERVER_PORT'] ); - exit(0); + @ob_flush(); exit(0); } if ( isset($c->restrict_admin_roles) && $roles == '' ) $roles = $c->restrict_admin_roles; if ( $this->logged_in && $roles == '' ) return; @@ -156,7 +156,7 @@ class DAViCalSession extends Session } include('page-footer.php'); - exit; + @ob_flush(); exit(0); } } diff --git a/inc/HTTPAuthSession.php b/inc/HTTPAuthSession.php index 4bad8fa3..12cf9c07 100644 --- a/inc/HTTPAuthSession.php +++ b/inc/HTTPAuthSession.php @@ -88,7 +88,7 @@ class HTTPAuthSession { header( $auth_header ); echo 'Please log in for access to this system.'; dbg_error_log( "HTTPAuth", ":Session: User is not authorised: %s ", $_SERVER['REMOTE_ADDR'] ); - exit; + @ob_flush(); exit(0); } @@ -139,6 +139,11 @@ class HTTPAuthSession { */ if ( isset($_SERVER['PHP_AUTH_USER']) ) { if ( $p = $this->CheckPassword( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] ) ) { + if ( isset($p->active) && !isset($p->user_active) ) { + trace_bug('Some authentication failed to return a dav_principal record and needs fixing.'); + $p->user_active = $p->active; + } + /** * Maybe some external authentication didn't return false for an inactive * user, so we'll be pedantic here. @@ -300,7 +305,11 @@ class HTTPAuthSession { * It can expect that: * - Configuration data will be in $c->authenticate_hook['config'], which might be an array, or whatever is needed. */ - return call_user_func( $c->authenticate_hook['call'], $username, $password ); + $principal = call_user_func( $c->authenticate_hook['call'], $username, $password ); + if ( $principal !== false && !($principal instanceof Principal) ) { + $principal = new Principal('username', $username); + } + return $principal; } return false; diff --git a/inc/WritableCollection.php b/inc/WritableCollection.php index 34e5b802..6d2bb91e 100644 --- a/inc/WritableCollection.php +++ b/inc/WritableCollection.php @@ -17,7 +17,7 @@ class WritableCollection extends DAVResource { return $p->GetParameterValue('TZID'); } - /** + /** * Writes the data to a member in the collection and returns the segment_name of the * resource in our internal namespace. * @@ -36,7 +36,7 @@ class WritableCollection extends DAVResource { return false; } - global $tz_regex, $session, $caldav_context; + global $session, $caldav_context; $resources = $vcal->GetComponents('VTIMEZONE',false); // Not matching VTIMEZONE $user_no = $this->user_no(); @@ -179,18 +179,11 @@ class WritableCollection extends DAVResource { $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 + * RFC2445, 4.8.1.3: Default is PUBLIC */ - if ( !isset($class) || $class == '' ) { + if ( $this->IsPublicOnly() || !isset($class) || $class == '' ) { $class = 'PUBLIC'; } $calitem_params[':class'] = $class; @@ -202,19 +195,11 @@ class WritableCollection extends DAVResource { $tz = $vcal->GetTimeZone($tzid); $olson = $vcal->GetOlsonName($tz); - dbg_error_log( 'PUT', ' Using TZID[%s] and location of [%s]', $tzid, (isset($olson) ? $olson : '') ); - if ( !empty($olson) && ($olson != $last_olson) && preg_match( $tz_regex, $olson ) ) { + if ( !empty($olson) && ($olson != $last_olson) ) { dbg_error_log( 'PUT', ' Setting timezone to %s', $olson ); $qry->QDo('SET TIMEZONE TO \''.$olson."'" ); $last_olson = $olson; } - $params = array( ':tzid' => $tzid); - $qry = new AwlQuery('SELECT 1 FROM timezones WHERE tzid = :tzid', $params ); - if ( $qry->Exec('PUT',__LINE__,__FILE__) && $qry->rows() == 0 ) { - $params[':olson_name'] = $olson; - $params[':vtimezone'] = (isset($tz) ? $tz->Render() : null ); - $qry->QDo('INSERT INTO timezones (tzid, olson_name, active, vtimezone) VALUES(:tzid,:olson_name,false,:vtimezone)', $params ); - } } $created = $first->GetPValue('CREATED'); @@ -258,8 +243,8 @@ EOSQL; } if ( !$this->IsSchedulingCollection() ) { - write_alarms($dav_id, $first); - write_attendees($dav_id, $vcal); + $this->WriteCalendarAlarms($dav_id, $vcal); + $this->WriteCalendarAttendees($dav_id, $vcal); if ( $log_action && function_exists('log_caldav_action') ) { log_caldav_action( $put_action_type, $first->GetPValue('UID'), $user_no, $collection_id, $path ); } @@ -317,4 +302,144 @@ EOSQL; return $segment_name; } + + /** + * Given a dav_id and an original vCalendar, pull out each of the VALARMs + * and write the values into the calendar_alarm table. + * + * @return null + */ + function WriteCalendarAlarms( $dav_id, vCalendar $vcal ) { + $qry = new AwlQuery('DELETE FROM calendar_alarm WHERE dav_id = '.$dav_id ); + $qry->Exec('PUT',__LINE__,__FILE__); + + $components = $vcal->GetComponents(); + + $qry->SetSql('INSERT INTO calendar_alarm ( dav_id, action, trigger, summary, description, component, next_trigger ) + VALUES( '.$dav_id.', :action, :trigger, :summary, :description, :component, + :related::timestamp with time zone + :related_trigger::interval )' ); + $qry->Prepare(); + foreach( $components AS $component ) { + if ( $component->GetType() == 'VTIMEZONE' ) continue; + $alarms = $component->GetComponents('VALARM'); + if ( count($alarms) < 1 ) return; + + foreach( $alarms AS $v ) { + $trigger = array_merge($v->GetProperties('TRIGGER')); + if ( $trigger == null ) continue; // Bogus data. + $trigger = $trigger[0]; + $related = null; + $related_trigger = '0M'; + $trigger_type = $trigger->GetParameterValue('VALUE'); + if ( !isset($trigger_type) || $trigger_type == 'DURATION' ) { + switch ( $trigger->GetParameterValue('RELATED') ) { + case 'DTEND': $related = $component->GetPValue('DTEND'); break; + case 'DUE': $related = $component->GetPValue('DUE'); break; + default: $related = $component->GetPValue('DTSTART'); + } + $duration = $trigger->Value(); + if ( !preg_match('{^-?P(:?\d+W)?(:?\d+D)?(:?T(:?\d+H)?(:?\d+M)?(:?\d+S)?)?$}', $duration ) ) continue; + $minus = (substr($duration,0,1) == '-'); + $related_trigger = trim(preg_replace( '#[PT-]#', ' ', $duration )); + if ( $minus ) { + $related_trigger = preg_replace( '{(\d+[WDHMS])}', '-$1 ', $related_trigger ); + } + else { + $related_trigger = preg_replace( '{(\d+[WDHMS])}', '$1 ', $related_trigger ); + } + } + else { + if ( false === strtotime($trigger->Value()) ) continue; // Invalid date. + } + $qry->Bind(':action', $v->GetPValue('ACTION')); + $qry->Bind(':trigger', $trigger->Render()); + $qry->Bind(':summary', $v->GetPValue('SUMMARY')); + $qry->Bind(':description', $v->GetPValue('DESCRIPTION')); + $qry->Bind(':component', $v->Render()); + $qry->Bind(':related', $related ); + $qry->Bind(':related_trigger', $related_trigger ); + $qry->Exec('PUT',__LINE__,__FILE__); + } + } + } + + + /** + * 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 vComponent The VEVENT or VTODO containing the ATTENDEEs + * @return null + */ + function WriteCalendarAttendees( $dav_id, vCalendar $vcal ) { + $qry = new AwlQuery('DELETE FROM calendar_attendee WHERE dav_id = '.$dav_id ); + $qry->Exec('PUT',__LINE__,__FILE__); + + $attendees = $vcal->GetAttendees(); + if ( count($attendees) < 1 ) return; + + $qry->SetSql('INSERT INTO calendar_attendee ( dav_id, status, partstat, cn, attendee, role, rsvp, property ) + VALUES( '.$dav_id.', :status, :partstat, :cn, :attendee, :role, :rsvp, :property )' ); + $qry->Prepare(); + $processed = array(); + foreach( $attendees AS $v ) { + $attendee = $v->Value(); + if ( isset($processed[$attendee]) ) { + dbg_error_log( 'LOG', 'Duplicate attendee "%s" in resource "%d"', $attendee, $dav_id ); + dbg_error_log( 'LOG', 'Original: "%s"', $processed[$attendee] ); + dbg_error_log( 'LOG', 'Duplicate: "%s"', $v->Render() ); + continue; /** @todo work out why we get duplicate ATTENDEE on one VEVENT */ + } + $qry->Bind(':attendee', $attendee ); + $qry->Bind(':status', $v->GetParameterValue('STATUS') ); + $qry->Bind(':partstat', $v->GetParameterValue('PARTSTAT') ); + $qry->Bind(':cn', $v->GetParameterValue('CN') ); + $qry->Bind(':role', $v->GetParameterValue('ROLE') ); + $qry->Bind(':rsvp', $v->GetParameterValue('RSVP') ); + $qry->Bind(':property', $v->Render() ); + $qry->Exec('PUT',__LINE__,__FILE__); + $processed[$attendee] = $v->Render(); + } + } + + /** + * Writes the data to a member in the collection and returns the segment_name of the + * resource in our internal namespace. + * + * @param vCalendar $member_dav_name The path to the resource to be deleted. + * @return boolean Success is true, or false on failure. + */ + function actualDeleteCalendarMember( $member_dav_name ) { + global $session, $caldav_context; + + // A quick sanity check... + $segment_name = str_replace( $this->dav_name(), '', $member_dav_name ); + if ( strstr($segment_name, '/') !== false ) { + @dbg_error_log( "DELETE", "DELETE: Refused to delete member '%s' from calendar '%s'!", $member_dav_name, $this->dav_name() ); + return false; + } + + // We need to serialise access to this process just for this collection + $cache = getCacheInstance(); + $myLock = $cache->acquireLock('collection-'.$this->dav_name()); + + $qry = new AwlQuery(); + $params = array( ':dav_name' => $member_dav_name ); + + if ( $qry->QDo("SELECT write_sync_change(collection_id, 404, caldav_data.dav_name) FROM caldav_data WHERE dav_name = :dav_name", $params ) + && $qry->QDo("DELETE FROM property WHERE dav_name = :dav_name", $params ) + && $qry->QDo("DELETE FROM locks WHERE dav_name = :dav_name", $params ) + && $qry->QDo("DELETE FROM caldav_data WHERE dav_name = :dav_name", $params ) ) { + @dbg_error_log( "DELETE", "DELETE: Calendar member %s deleted from calendar '%s'", $member_dav_name, $this->dav_name() ); + + $cache->releaseLock($myLock); + + return true; + } + + $cache->releaseLock($myLock); + return false; + + } + } diff --git a/inc/always.php.in b/inc/always.php.in index 05a9ac22..b1c32d23 100644 --- a/inc/always.php.in +++ b/inc/always.php.in @@ -26,6 +26,7 @@ function early_exception_handler($e) { foreach( $trace AS $k => $v ) { 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']:'') ); } + @ob_flush(); } set_exception_handler('early_exception_handler'); @@ -89,7 +90,7 @@ if ( ! @include_once('AWLUtilities.php') ) { } if ( ! @include_once('AWLUtilities.php') ) { echo "Could not find the AWL libraries. Are they installed? Check your include_path in php.ini!\n"; - exit; + @ob_flush(); exit(0); } } @@ -145,7 +146,7 @@ else if ( @file_exists('config/config.php') ) { } else { include('davical_configuration_missing.php'); - exit; + @ob_flush(); exit(0); } $config_warnings = trim(ob_get_contents()); ob_end_clean(); diff --git a/inc/auth-functions.php b/inc/auth-functions.php index 6ada2c2a..b716f2f6 100644 --- a/inc/auth-functions.php +++ b/inc/auth-functions.php @@ -299,7 +299,7 @@ function AuthExternalAWL( $username, $password ) {