/** * Create a new CalDAVRequest object. */ function __construct($options = array()) { global $session, $c, $debugging; $this->options = $options; if (!isset($this->options['allow_by_email'])) { $this->options['allow_by_email'] = false; } if (isset($_SERVER['HTTP_PREFER'])) { $this->prefer = explode(',', $_SERVER['HTTP_PREFER']); } else { if (isset($_SERVER['HTTP_BRIEF']) && strtoupper($_SERVER['HTTP_BRIEF']) == 'T') { $this->prefer = array('return-minimal'); } else { $this->prefer = array(); } } /** * Our path is /<script name>/<user name>/<user controlled> if it ends in * a trailing '/' then it is referring to a DAV 'collection' but otherwise * it is referring to a DAV data item. * * Permissions are controlled as follows: * 1. if there is no <user name> component, the request has read privileges * 2. if the requester is an admin, the request has read/write priviliges * 3. if there is a <user name> component which matches the logged on user * then the request has read/write privileges * 4. otherwise we query the defined relationships between users and use * the minimum privileges returned from that analysis. */ if (isset($_SERVER['PATH_INFO'])) { $this->path = $_SERVER['PATH_INFO']; } else { $this->path = '/'; if (isset($_SERVER['REQUEST_URI'])) { if (preg_match('{^(.*?\\.php)(.*)$}', $_SERVER['REQUEST_URI'], $matches)) { $this->path = $matches[2]; if (substr($this->path, 0, 1) != '/') { $this->path = '/' . $this->path; } } else { if ($_SERVER['REQUEST_URI'] != '/') { dbg_error_log('LOG', 'Server is not supplying PATH_INFO and REQUEST_URI does not include a PHP program. Wildly guessing "/"!!!'); } } } } $this->path = rawurldecode($this->path); /** Allow a request for .../calendar.ics to translate into the calendar URL */ if (preg_match('#^(/[^/]+/[^/]+).ics$#', $this->path, $matches)) { $this->path = $matches[1] . '/'; } if (isset($c->replace_path) && isset($c->replace_path['from']) && isset($c->replace_path['to'])) { $this->path = preg_replace($c->replace_path['from'], $c->replace_path['to'], $this->path); } // dbg_error_log( "caldav", "Sanitising path '%s'", $this->path ); $bad_chars_regex = '/[\\^\\[\\(\\\\]/'; if (preg_match($bad_chars_regex, $this->path)) { $this->DoResponse(400, translate("The calendar path contains illegal characters.")); } if (strstr($this->path, '//')) { $this->path = preg_replace('#//+#', '/', $this->path); } if (!isset($c->raw_post)) { $c->raw_post = file_get_contents('php://input'); } if (isset($_SERVER['HTTP_CONTENT_ENCODING'])) { $encoding = $_SERVER['HTTP_CONTENT_ENCODING']; @dbg_error_log('caldav', 'Content-Encoding: %s', $encoding); $encoding = preg_replace('{[^a-z0-9-]}i', '', $encoding); if (!ini_get('open_basedir') && (isset($c->dbg['ALL']) || isset($c->dbg['caldav']))) { $fh = fopen('/var/log/davical/encoded_data.debug' . $encoding, 'w'); if ($fh) { fwrite($fh, $c->raw_post); fclose($fh); } } switch ($encoding) { case 'gzip': $this->raw_post = @gzdecode($c->raw_post); break; case 'deflate': $this->raw_post = @gzinflate($c->raw_post); break; case 'compress': $this->raw_post = @gzuncompress($c->raw_post); break; default: } if (empty($this->raw_post) && !empty($c->raw_post)) { $this->PreconditionFailed(415, 'content-encoding', sprintf('Unable to decode "%s" content encoding.', $_SERVER['HTTP_CONTENT_ENCODING'])); } $c->raw_post = $this->raw_post; } else { $this->raw_post = $c->raw_post; } if (isset($debugging) && isset($_GET['method'])) { $_SERVER['REQUEST_METHOD'] = $_GET['method']; } else { if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) { $_SERVER['REQUEST_METHOD'] = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']; } } $this->method = $_SERVER['REQUEST_METHOD']; $this->content_type = isset($_SERVER['CONTENT_TYPE']) ? $_SERVER['CONTENT_TYPE'] : null; if (preg_match('{^(\\S+/\\S+)\\s*(;.*)?$}', $this->content_type, $matches)) { $this->content_type = $matches[1]; } if (strlen($c->raw_post) > 0) { if ($this->method == 'PROPFIND' || $this->method == 'REPORT' || $this->method == 'PROPPATCH' || $this->method == 'BIND' || $this->method == 'MKTICKET' || $this->method == 'ACL') { if (!preg_match('{^(text|application)/xml$}', $this->content_type)) { @dbg_error_log("LOG request", 'Request is "%s" but client set content-type to "%s". Assuming they meant XML!', $this->method, $this->content_type); $this->content_type = 'text/xml'; } } else { if ($this->method == 'PUT' || $this->method == 'POST') { $this->CoerceContentType(); } } } else { $this->content_type = 'text/plain'; } $this->user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : "Probably Mulberry"; /** * A variety of requests may set the "Depth" header to control recursion */ if (isset($_SERVER['HTTP_DEPTH'])) { $this->depth = $_SERVER['HTTP_DEPTH']; } else { /** * Per rfc2518, section 9.2, 'Depth' might not always be present, and if it * is not present then a reasonable request-type-dependent default should be * chosen. */ switch ($this->method) { case 'DELETE': case 'MOVE': case 'COPY': case 'LOCK': $this->depth = 'infinity'; break; case 'REPORT': $this->depth = 0; break; case 'PROPFIND': default: $this->depth = 0; } } if (!is_int($this->depth) && "infinity" == $this->depth) { $this->depth = DEPTH_INFINITY; } $this->depth = intval($this->depth); /** * MOVE/COPY use a "Destination" header and (optionally) an "Overwrite" one. */ if (isset($_SERVER['HTTP_DESTINATION'])) { $this->destination = $_SERVER['HTTP_DESTINATION']; if (preg_match('{^(https?)://([a-z.-]+)(:[0-9]+)?(/.*)$}', $this->destination, $matches)) { $this->destination = $matches[4]; } } $this->overwrite = isset($_SERVER['HTTP_OVERWRITE']) && $_SERVER['HTTP_OVERWRITE'] == 'F' ? false : true; // RFC4918, 9.8.4 says default True. /** * LOCK things use an "If" header to hold the lock in some cases, and "Lock-token" in others */ if (isset($_SERVER['HTTP_IF'])) { $this->if_clause = $_SERVER['HTTP_IF']; } if (isset($_SERVER['HTTP_LOCK_TOKEN']) && preg_match('#[<]opaquelocktoken:(.*)[>]#', $_SERVER['HTTP_LOCK_TOKEN'], $matches)) { $this->lock_token = $matches[1]; } /** * Check for an access ticket. */ if (isset($_GET['ticket'])) { $this->ticket = new DAVTicket($_GET['ticket']); } else { if (isset($_SERVER['HTTP_TICKET'])) { $this->ticket = new DAVTicket($_SERVER['HTTP_TICKET']); } } /** * LOCK things use a "Timeout" header to set a series of reducing alternative values */ if (isset($_SERVER['HTTP_TIMEOUT'])) { $timeouts = explode(',', $_SERVER['HTTP_TIMEOUT']); foreach ($timeouts as $k => $v) { if (strtolower($v) == 'infinite') { $this->timeout = isset($c->maximum_lock_timeout) ? $c->maximum_lock_timeout : 86400 * 100; break; } elseif (strtolower(substr($v, 0, 7)) == 'second-') { $this->timeout = min(intval(substr($v, 7)), isset($c->maximum_lock_timeout) ? $c->maximum_lock_timeout : 86400 * 100); break; } } if (!isset($this->timeout) || $this->timeout == 0) { $this->timeout = isset($c->default_lock_timeout) ? $c->default_lock_timeout : 900; } } $this->principal = new Principal('path', $this->path); /** * RFC2518, 5.2: URL pointing to a collection SHOULD end in '/', and if it does not then * we SHOULD return a Content-location header with the correction... * * We therefore look for a collection which matches one of the following URLs: * - The exact request. * - If the exact request, doesn't end in '/', then the request URL with a '/' appended * - The request URL truncated to the last '/' * The collection URL for this request is therefore the longest row in the result, so we * can "... ORDER BY LENGTH(dav_name) DESC LIMIT 1" */ $sql = "SELECT * FROM collection WHERE dav_name = :exact_name"; $params = array(':exact_name' => $this->path); if (!preg_match('#/$#', $this->path)) { $sql .= " OR dav_name = :truncated_name OR dav_name = :trailing_slash_name"; $params[':truncated_name'] = preg_replace('#[^/]*$#', '', $this->path); $params[':trailing_slash_name'] = $this->path . "/"; } $sql .= " ORDER BY LENGTH(dav_name) DESC LIMIT 1"; $qry = new AwlQuery($sql, $params); if ($qry->Exec('caldav', __LINE__, __FILE__) && $qry->rows() == 1 && ($row = $qry->Fetch())) { if ($row->dav_name == $this->path . "/") { $this->path = $row->dav_name; dbg_error_log("caldav", "Path is actually a collection - sending Content-Location header."); header("Content-Location: " . ConstructURL($this->path)); } $this->collection_id = $row->collection_id; $this->collection_path = $row->dav_name; $this->collection_type = $row->is_calendar == 't' ? 'calendar' : 'collection'; $this->collection = $row; if (preg_match('#^((/[^/]+/)\\.(in|out)/)[^/]*$#', $this->path, $matches)) { $this->collection_type = 'schedule-' . $matches[3] . 'box'; } $this->collection->type = $this->collection_type; } else { if (preg_match('{^( ( / ([^/]+) / ) \\.(in|out)/ ) [^/]*$}x', $this->path, $matches)) { // The request is for a scheduling inbox or outbox (or something inside one) and we should auto-create it $params = array(':username' => $matches[3], ':parent_container' => $matches[2], ':dav_name' => $matches[1]); $params[':boxname'] = $matches[4] == 'in' ? ' Inbox' : ' Outbox'; $this->collection_type = 'schedule-' . $matches[4] . 'box'; $params[':resourcetypes'] = sprintf('<DAV::collection/><urn:ietf:params:xml:ns:caldav:%s/>', $this->collection_type); $sql = <<<EOSQL INSERT INTO collection ( user_no, parent_container, dav_name, dav_displayname, is_calendar, created, modified, dav_etag, resourcetypes ) VALUES( (SELECT user_no FROM usr WHERE username = text(:username)), :parent_container, :dav_name, (SELECT fullname FROM usr WHERE username = text(:username)) || :boxname, FALSE, current_timestamp, current_timestamp, '1', :resourcetypes ) EOSQL; $qry = new AwlQuery($sql, $params); $qry->Exec('caldav', __LINE__, __FILE__); dbg_error_log('caldav', 'Created new collection as "%s".', trim($params[':boxname'])); // Uncache anything to do with the collection $cache = getCacheInstance(); $cache->delete('collection-' . $params[':dav_name'], null); $cache->delete('principal-' . $params[':parent_container'], null); $qry = new AwlQuery("SELECT * FROM collection WHERE dav_name = :dav_name", array(':dav_name' => $matches[1])); if ($qry->Exec('caldav', __LINE__, __FILE__) && $qry->rows() == 1 && ($row = $qry->Fetch())) { $this->collection_id = $row->collection_id; $this->collection_path = $matches[1]; $this->collection = $row; $this->collection->type = $this->collection_type; } } else { if (preg_match('#^((/[^/]+/)calendar-proxy-(read|write))/?[^/]*$#', $this->path, $matches)) { $this->collection_type = 'proxy'; $this->_is_proxy_request = true; $this->proxy_type = $matches[3]; $this->collection_path = $matches[1] . '/'; // Enforce trailling '/' if ($this->collection_path == $this->path . "/") { $this->path .= '/'; dbg_error_log("caldav", "Path is actually a (proxy) collection - sending Content-Location header."); header("Content-Location: " . ConstructURL($this->path)); } } else { if ($this->options['allow_by_email'] && preg_match('#^/(\\S+@\\S+[.]\\S+)/?$#', $this->path)) { /** @todo we should deprecate this now that Evolution 2.27 can do scheduling extensions */ $this->collection_id = -1; $this->collection_type = 'email'; $this->collection_path = $this->path; $this->_is_principal = true; } else { if (preg_match('#^(/[^/?]+)/?$#', $this->path, $matches) || preg_match('#^(/principals/[^/]+/[^/]+)/?$#', $this->path, $matches)) { $this->collection_id = -1; $this->collection_path = $matches[1] . '/'; // Enforce trailling '/' $this->collection_type = 'principal'; $this->_is_principal = true; if ($this->collection_path == $this->path . "/") { $this->path .= '/'; dbg_error_log("caldav", "Path is actually a collection - sending Content-Location header."); header("Content-Location: " . ConstructURL($this->path)); } if (preg_match('#^(/principals/[^/]+/[^/]+)/?$#', $this->path, $matches)) { // Force a depth of 0 on these, which are at the wrong URL. $this->depth = 0; } } else { if ($this->path == '/') { $this->collection_id = -1; $this->collection_path = '/'; $this->collection_type = 'root'; } } } } } } if ($this->collection_path == $this->path) { $this->_is_collection = true; } dbg_error_log("caldav", " Collection '%s' is %d, type %s", $this->collection_path, $this->collection_id, $this->collection_type); /** * Extract the user whom we are accessing */ $this->principal = new DAVPrincipal(array("path" => $this->path, "options" => $this->options)); $this->user_no = $this->principal->user_no(); $this->username = $this->principal->username(); $this->by_email = $this->principal->byEmail(); $this->principal_id = $this->principal->principal_id(); if ($this->collection_type == 'principal' || $this->collection_type == 'email' || $this->collection_type == 'proxy') { $this->collection = $this->principal->AsCollection(); if ($this->collection_type == 'proxy') { $this->collection = $this->principal->AsCollection(); $this->collection->is_proxy = 't'; $this->collection->type = 'proxy'; $this->collection->proxy_type = $this->proxy_type; $this->collection->dav_displayname = sprintf('Proxy %s for %s', $this->proxy_type, $this->principal->username()); } } elseif ($this->collection_type == 'root') { $this->collection = (object) array('collection_id' => 0, 'dav_name' => '/', 'dav_etag' => md5($c->system_name), 'is_calendar' => 'f', 'is_addressbook' => 'f', 'is_principal' => 'f', 'user_no' => 0, 'dav_displayname' => $c->system_name, 'type' => 'root', 'created' => date('Ymd\\THis')); } /** * Evaluate our permissions for accessing the target */ $this->setPermissions(); $this->supported_methods = array('OPTIONS' => '', 'PROPFIND' => '', 'REPORT' => '', 'DELETE' => '', 'LOCK' => '', 'UNLOCK' => '', 'MOVE' => '', 'ACL' => ''); if ($this->IsCollection()) { switch ($this->collection_type) { case 'root': case 'email': // We just override the list completely here. $this->supported_methods = array('OPTIONS' => '', 'PROPFIND' => '', 'REPORT' => ''); break; case 'schedule-inbox': case 'schedule-outbox': $this->supported_methods = array_merge($this->supported_methods, array('POST' => '', 'GET' => '', 'PUT' => '', 'HEAD' => '', 'PROPPATCH' => '')); break; case 'calendar': $this->supported_methods['GET'] = ''; $this->supported_methods['PUT'] = ''; $this->supported_methods['HEAD'] = ''; break; case 'collection': case 'principal': $this->supported_methods['GET'] = ''; $this->supported_methods['PUT'] = ''; $this->supported_methods['HEAD'] = ''; $this->supported_methods['MKCOL'] = ''; $this->supported_methods['MKCALENDAR'] = ''; $this->supported_methods['PROPPATCH'] = ''; $this->supported_methods['BIND'] = ''; break; } } else { $this->supported_methods = array_merge($this->supported_methods, array('GET' => '', 'HEAD' => '', 'PUT' => '')); } $this->supported_reports = array('DAV::principal-property-search' => '', 'DAV::expand-property' => '', 'DAV::sync-collection' => ''); if (isset($this->collection) && $this->collection->is_calendar) { $this->supported_reports = array_merge($this->supported_reports, array('urn:ietf:params:xml:ns:caldav:calendar-query' => '', 'urn:ietf:params:xml:ns:caldav:calendar-multiget' => '', 'urn:ietf:params:xml:ns:caldav:free-busy-query' => '')); } if (isset($this->collection) && $this->collection->is_addressbook) { $this->supported_reports = array_merge($this->supported_reports, array('urn:ietf:params:xml:ns:carddav:addressbook-query' => '', 'urn:ietf:params:xml:ns:carddav:addressbook-multiget' => '')); } /** * If the content we are receiving is XML then we parse it here. RFC2518 says we * should reasonably expect to see either text/xml or application/xml */ if (isset($this->content_type) && preg_match('#(application|text)/xml#', $this->content_type)) { if (!isset($this->raw_post) || $this->raw_post == '') { $this->XMLResponse(400, new XMLElement('error', new XMLElement('missing-xml'), array('xmlns' => 'DAV:'))); } $xml_parser = xml_parser_create_ns('UTF-8'); $this->xml_tags = array(); xml_parser_set_option($xml_parser, XML_OPTION_SKIP_WHITE, 1); xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, 0); $rc = xml_parse_into_struct($xml_parser, $this->raw_post, $this->xml_tags); if ($rc == false) { dbg_error_log('ERROR', 'XML parsing error: %s at line %d, column %d', xml_error_string(xml_get_error_code($xml_parser)), xml_get_current_line_number($xml_parser), xml_get_current_column_number($xml_parser)); $this->XMLResponse(400, new XMLElement('error', new XMLElement('invalid-xml'), array('xmlns' => 'DAV:'))); } xml_parser_free($xml_parser); if (count($this->xml_tags)) { dbg_error_log("caldav", " Parsed incoming XML request body."); } else { $this->xml_tags = null; dbg_error_log("ERROR", "Incoming request sent content-type XML with no XML request body."); } } /** * Look out for If-None-Match or If-Match headers */ if (isset($_SERVER["HTTP_IF_NONE_MATCH"])) { $this->etag_none_match = $_SERVER["HTTP_IF_NONE_MATCH"]; if ($this->etag_none_match == '') { unset($this->etag_none_match); } } if (isset($_SERVER["HTTP_IF_MATCH"])) { $this->etag_if_match = $_SERVER["HTTP_IF_MATCH"]; if ($this->etag_if_match == '') { unset($this->etag_if_match); } } }
/** * Do the scheduling adjustments for a REPLY when an ATTENDEE updates their status. * @param vCalendar $resource The resource that the ATTENDEE is writing to their calendar * @param string $organizer The property which is the event ORGANIZER. */ function do_scheduling_reply(vCalendar $resource, vProperty $organizer) { global $request; $organizer_email = preg_replace('/^mailto:/i', '', $organizer->Value()); $organizer_principal = new Principal('email', $organizer_email); if (!$organizer_principal->Exists()) { dbg_error_log('PUT', 'Organizer "%s" not found - cannot perform scheduling reply.', $organizer); return false; } $sql = 'SELECT caldav_data.dav_name, caldav_data.caldav_data FROM caldav_data JOIN calendar_item USING(dav_id) '; $sql .= 'WHERE caldav_data.collection_id IN (SELECT collection_id FROM collection WHERE is_calendar AND user_no =?) '; $sql .= 'AND uid=? LIMIT 1'; $uids = $resource->GetPropertiesByPath('/VCALENDAR/*/UID'); if (count($uids) == 0) { dbg_error_log('PUT', 'No UID in VCALENDAR - giving up on REPLY.'); return false; } $uid = $uids[0]->Value(); $qry = new AwlQuery($sql, $organizer_principal->user_no(), $uid); if (!$qry->Exec('PUT', __LINE__, __FILE__) || $qry->rows() < 1) { dbg_error_log('PUT', 'Could not find original event from organizer - giving up on REPLY.'); return false; } $row = $qry->Fetch(); $attendees = $resource->GetAttendees(); foreach ($attendees as $v) { $email = preg_replace('/^mailto:/i', '', $v->Value()); if ($email == $request->principal->email()) { $attendee = $v; } } if (empty($attendee)) { dbg_error_log('PUT', 'Could not find ATTENDEE in VEVENT - giving up on REPLY.'); return false; } $schedule_original = new vCalendar($row->caldav_data); $schedule_original->UpdateAttendeeStatus($request->principal->email(), clone $attendee); $collection_path = preg_replace('{/[^/]+$}', '/', $row->dav_name); $segment_name = str_replace($collection_path, '', $row->dav_name); $organizer_calendar = new WritableCollection(array('path' => $collection_path)); $organizer_inbox = new WritableCollection(array('path' => $organizer_principal->internal_url('schedule-inbox'))); $schedule_reply = clone $schedule_original; $schedule_reply->AddProperty('METHOD', 'REPLY'); dbg_error_log('PUT', 'Writing scheduling REPLY from %s to %s', $request->principal->email(), $organizer_principal->email()); $response = '3.7'; // Organizer was not found on server. if (!$organizer_calendar->Exists()) { dbg_error_log('ERROR', 'Default calendar at "%s" does not exist for user "%s"', $organizer_calendar->dav_name(), $schedule_target->username()); $response = '5.2'; // No scheduling support for user } else { if (!$organizer_inbox->HavePrivilegeTo('schedule-deliver-reply')) { $response = '3.8'; // No authority to deliver replies to organizer. } else { if ($organizer_inbox->WriteCalendarMember($schedule_reply, false, false, $request->principal->username() . $segment_name) !== false) { $response = '1.2'; // Scheduling reply delivered successfully if ($organizer_calendar->WriteCalendarMember($schedule_original, false, false, $segment_name) === false) { dbg_error_log('ERROR', 'Could not write updated calendar member to %s', $attendee_calendar->dav_name(), $attendee_calendar->dav_name(), $schedule_target->username()); trace_bug('Failed to write scheduling resource.'); } } } } $schedule_request = clone $schedule_original; $schedule_request->AddProperty('METHOD', 'REQUEST'); dbg_error_log('PUT', 'Status for organizer <%s> set to "%s"', $organizer->Value(), $response); $organizer->SetParameterValue('SCHEDULE-STATUS', $response); $resource->UpdateOrganizerStatus($organizer); $scheduling_actions = true; $calling_attendee = clone $attendee; $attendees = $schedule_original->GetAttendees(); foreach ($attendees as $attendee) { $email = preg_replace('/^mailto:/i', '', $attendee->Value()); if ($email == $request->principal->email() || $email == $organizer_principal->email()) { continue; } $agent = $attendee->GetParameterValue('SCHEDULE-AGENT'); if ($agent && $agent != 'SERVER') { dbg_error_log("PUT", "not delivering to %s, schedule agent set to value other than server", $email); continue; } // an attendee's reply should modify only the PARTSTAT on other attendees' objects // other properties (that might have been adjusted individually by those other // attendees) should remain unmodified. Therefore, we have to make $schedule_original // and $schedule_request be initialized by each attendee's object here. $attendee_principal = new DAVPrincipal(array('email' => $email, 'options' => array('allow_by_email' => true))); if ($attendee_principal == false) { dbg_error_log('PUT', 'Could not find attendee %s', $email); continue; } $sql = 'SELECT caldav_data.dav_name, caldav_data.caldav_data, caldav_data.collection_id FROM caldav_data JOIN calendar_item USING(dav_id) '; $sql .= 'WHERE caldav_data.collection_id IN (SELECT collection_id FROM collection WHERE is_calendar AND user_no =?) '; $sql .= 'AND uid=? LIMIT 1'; $qry = new AwlQuery($sql, $attendee_principal->user_no(), $uid); if (!$qry->Exec('PUT', __LINE__, __FILE__) || $qry->rows() < 1) { dbg_error_log('PUT', "Could not find attendee's event %s", $uid); } $row = $qry->Fetch(); $schedule_original = new vCalendar($row->caldav_data); $schedule_original->UpdateAttendeeStatus($request->principal->email(), clone $calling_attendee); $schedule_request = clone $schedule_original; $schedule_request->AddProperty('METHOD', 'REQUEST'); $schedule_target = new Principal('email', $email); $response = '3.7'; // Attendee was not found on server. if ($schedule_target->Exists()) { // Instead of always writing to schedule-default-calendar, we first try to // find a calendar with an existing instance of the event in any calendar of this attendee. $r = new DAVResource($row); $attendee_calendar = new WritableCollection(array('path' => $r->parent_path())); if ($attendee_calendar->IsCalendar()) { dbg_error_log('XXX', "found the event in attendee's calendar %s", $attendee_calendar->dav_name()); } else { dbg_error_log('XXX', 'could not find the event in any calendar, using schedule-default-calendar'); $attendee_calendar = new WritableCollection(array('path' => $schedule_target->internal_url('schedule-default-calendar'))); } if (!$attendee_calendar->Exists()) { dbg_error_log('ERROR', 'Default calendar at "%s" does not exist for user "%s"', $attendee_calendar->dav_name(), $schedule_target->username()); $response = '5.2'; // No scheduling support for user } else { $attendee_inbox = new WritableCollection(array('path' => $schedule_target->internal_url('schedule-inbox'))); if (!$attendee_inbox->HavePrivilegeTo('schedule-deliver-invite')) { $response = '3.8'; // No authority to deliver invitations to user. } else { if ($attendee_inbox->WriteCalendarMember($schedule_request, false) !== false) { $response = '1.2'; // Scheduling invitation delivered successfully if ($attendee_calendar->WriteCalendarMember($schedule_original, false) === false) { dbg_error_log('ERROR', 'Could not write updated calendar member to %s', $attendee_calendar->dav_name(), $attendee_calendar->dav_name(), $schedule_target->username()); trace_bug('Failed to write scheduling resource.'); } } } } } dbg_error_log('PUT', 'Status for attendee <%s> set to "%s"', $attendee->Value(), $response); $attendee->SetParameterValue('SCHEDULE-STATUS', $response); $scheduling_actions = true; $resource->UpdateAttendeeStatus($email, clone $attendee); } return $scheduling_actions; }