/** * 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); } } }
$qry->Exec('move'); // Just in case $request->DoResponse($response_code); // And we don't return from that. } $qry = new AwlQuery('BEGIN'); if (!$qry->Exec('move')) { rollback(500); } $src_name = $src->dav_name(); $dst_name = $dest->IsBinding() ? $dest->bound_from() : $dest->dav_name(); $src_collection = $src->GetProperty('collection_id'); $dst_collection = $dest->GetProperty('collection_id'); $src_user_no = $src->GetProperty('user_no'); $dst_user_no = $dest->GetProperty('user_no'); $cache = getCacheInstance(); $cachekeys = array(); if ($src->IsCollection()) { $cachekeys[] = ($src->ContainerType() == 'principal' ? 'principal' : 'collection') . '-' . $src->parent_path(); $cachekeys[] = ($src->IsPrincipal() == 'principal' ? 'principal' : 'collection') . '-' . $src->dav_name(); $cachekeys[] = ($src->IsPrincipal() ? 'principal' : 'collection') . '-' . $dest->dav_name(); if ($dest->Exists()) { $qry = new AwlQuery('DELETE FROM collection WHERE dav_name = :dst_name', array(':dst_name' => $dst_name)); if (!$qry->Exec('move')) { rollback(500); } } /** @todo Need to confirm this will work correctly if we move this into another user's hierarchy. */ $sql = 'UPDATE collection SET dav_name = :dst_name '; $params = array(':dst_name' => $dst_name); if ($src_user_no != $dst_user_no) {
/** * Actually write the resource to the database. All checking of whether this is reasonable * should be done before this is called. * * @param DAVResource $resource The resource being written * @param string $caldav_data The actual data to be written * @param DAVResource $collection 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 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 * @param boolean $log_action Whether to log the fact that we are writing this into an action log (if configured) * @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(DAVResource $resource, $caldav_data, DAVResource $collection, $author, &$etag, $put_action_type, $caldav_context, $log_action = true, $weak_etag = null) { global $tz_regex, $session; $path = $resource->bound_from(); $user_no = $collection->user_no(); $vcal = new vCalendar($caldav_data); $resources = $vcal->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]; if (!$first instanceof vComponent) { print $vcal->Render(); fatal('This is not a vComponent!'); } $resource_type = $first->GetType(); } $collection_id = $collection->collection_id(); $qry = new AwlQuery(); $qry->Begin(); $dav_params = array(':etag' => $etag, ':dav_data' => $caldav_data, ':caldav_type' => $resource_type, ':session_user' => $author, ':weak_etag' => $weak_etag); $calitem_params = array(':etag' => $etag); if ($put_action_type == 'INSERT') { $qry->QDo('SELECT nextval(\'dav_id_seq\') AS dav_id, null AS caldav_data'); } else { $qry->QDo('SELECT dav_id, caldav_data 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! trace_bug('No dav_id for "%s" on %s!!!', $path, $create_resource ? 'create' : 'update'); rollback_on_error($caldav_context, $user_no, $path); return false; } $dav_id = $row->dav_id; $old_dav_data = $row->caldav_data; $dav_params[':dav_id'] = $dav_id; $calitem_params[':dav_id'] = $dav_id; $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')); $calitem_params[':dtend'] = $dtend; $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'))); if ($duration == '') { $duration = '0 seconds'; } $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. * */ $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)'; } else { $dtend = ':dtstart'; } } } $dtstamp = $first->GetPValue('DTSTAMP'); if (!isset($dtstamp) || $dtstamp == '') { // Strictly, we're dealing with an out of spec component here, but we'll try and survive $dtstamp = gmdate('Ymd\\THis\\Z'); } $calitem_params[':dtstamp'] = $dtstamp; $last_modified = $first->GetPValue('LAST-MODIFIED'); if (!isset($last_modified) || $last_modified == '') { $last_modified = $dtstamp; } $dav_params[':modified'] = $last_modified; $calitem_params[':modified'] = $last_modified; $created = $first->GetPValue('CREATED'); if ($created == '00001231T000000Z') { $created = '20001231T000000Z'; } $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_olson = 'Turkmenikikamukau'; // I really hope this location doesn't exist! $tzid = GetTZID($first); if (!empty($tzid)) { $timezones = $vcal->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; } $olson = olson_from_tzstring($tzid); if (empty($olson)) { $olson = $tz->GetPValue('X-LIC-LOCATION'); if (!empty($olson)) { $olson = olson_from_tzstring($olson); } } } 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)) { dbg_error_log('PUT', ' Setting timezone to %s', $olson); if ($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); } if (!isset($olson) || $olson == '') { $olson = $tzid; } } $qry->QDo('SELECT new_sync_token(0,' . $collection_id . ')'); $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[':percent_complete'] = $first->GetPValue('PERCENT-COMPLETE'); $calitem_params[':status'] = $first->GetPValue('STATUS'); if (!$collection->IsSchedulingCollection()) { if (do_scheduling_requests($vcal, $put_action_type == 'INSERT', $old_dav_data, true)) { $dav_params[':dav_data'] = $vcal->Render(null, true); $etag = null; } } if (!isset($dav_params[':modified'])) { $dav_params[':modified'] = 'now'; } if ($put_action_type == 'INSERT') { $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; $dav_params[':user_no'] = $user_no; $dav_params[':dav_name'] = $path; $dav_params[':created'] = isset($created) && $created != '' ? $created : $dtstamp; } else { $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'; } $qry = new AwlQuery($sql, $dav_params); if (!$qry->Exec('PUT', __LINE__, __FILE__)) { fatal('Insert into calendar_item failed...'); rollback_on_error($caldav_context, $user_no, $path); return false; } if ($put_action_type == 'INSERT') { $sql = <<<EOSQL 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, :dav_id, :etag, :uid, :dtstamp, :dtstart, {$dtend}, :summary, :location, :class, :transp, :description, :rrule, :tzid, :modified, :url, :priority, :created, :due, :percent_complete, :status, :collection_id ) EOSQL; $sync_change = 201; $calitem_params[':collection_id'] = $collection_id; $calitem_params[':user_no'] = $user_no; $calitem_params[':dav_name'] = $path; $calitem_params[':created'] = $dav_params[':created']; } else { $sql = <<<EOSQL UPDATE calendar_item SET dav_etag=:etag, uid=:uid, dtstamp=:dtstamp, dtstart=:dtstart, dtend={$dtend}, summary=:summary, location=:location, class=:class, transp=:transp, description=:description, rrule=:rrule, tz_id=:tzid, last_modified=:modified, url=:url, priority=:priority, due=:due, percent_complete=:percent_complete, status=:status WHERE dav_id=:dav_id EOSQL; $sync_change = 200; } write_alarms($dav_id, $first); write_attendees($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); } 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)); $qry->Commit(); if (function_exists('post_commit_action')) { post_commit_action($put_action_type, $first->GetPValue('UID'), $user_no, $collection_id, $path); } // Uncache anything to do with the collection $cache = getCacheInstance(); $cache_ns = 'collection-' . preg_replace('{/[^/]*$}', '/', $path); $cache->delete($cache_ns, null); dbg_error_log('PUT', 'User: %d, ETag: %s, Path: %s', $author, $etag, $path); return true; // Success! }
function caldav_get_feed($request, $collection) { global $c, $session; dbg_error_log("feed", "GET method handler"); $collection->NeedPrivilege(array('DAV::read')); if (!$collection->Exists()) { $request->DoResponse(404, translate("Resource Not Found.")); } if (!$collection->IsCollection() || !$collection->IsCalendar() && !(isset($c->get_includes_subcollections) && $c->get_includes_subcollections)) { $request->DoResponse(405, translate("Feeds are only supported for calendars at present.")); } // Try and pull the answer out of a hat $cache = getCacheInstance(); $cache_ns = 'collection-' . $collection->dav_name(); $cache_key = 'feed' . $session->user_no; $response = $cache->get($cache_ns, $cache_key); if ($response !== false) { return $response; } $principal = $collection->GetProperty('principal'); /** * The CalDAV specification does not define GET on a collection, but typically this is * used as a .ics download for the whole collection, which is what we do also. */ $sql = 'SELECT caldav_data, caldav_type, caldav_data.user_no, caldav_data.dav_name,'; $sql .= ' caldav_data.modified, caldav_data.created, '; $sql .= ' summary, dtstart, dtend, calendar_item.description '; $sql .= ' FROM collection INNER JOIN caldav_data USING(collection_id) INNER JOIN calendar_item USING ( dav_id ) WHERE '; if (isset($c->get_includes_subcollections) && $c->get_includes_subcollections) { $sql .= ' (collection.dav_name ~ :path_match '; $sql .= ' OR collection.collection_id IN (SELECT bound_source_id FROM dav_binding WHERE dav_binding.dav_name ~ :path_match)) '; $params = array(':path_match' => '^' . $request->path); } else { $sql .= ' caldav_data.collection_id = :collection_id '; $params = array(':collection_id' => $collection->resource_id()); } $sql .= ' ORDER BY caldav_data.created DESC'; $sql .= ' LIMIT ' . (isset($c->feed_item_limit) ? $c->feed_item_limit : 15); $qry = new AwlQuery($sql, $params); if (!$qry->Exec("GET", __LINE__, __FILE__)) { $request->DoResponse(500, translate("Database Error")); } /** * Here we are constructing the feed response for this collection, including * the timezones that are referred to by the events we have selected. * Library used: http://framework.zend.com/manual/en/zend.feed.writer.html */ require_once 'AtomFeed.php'; $feed = new AtomFeed(); $feed->setTitle('DAViCal Atom Feed: ' . $collection->GetProperty('displayname')); $url = $c->protocol_server_port . $collection->url(); $url = preg_replace('{/$}', '.ics', $url); $feed->setLink($url); $feed->setFeedLink($c->protocol_server_port_script . $request->path, 'atom'); $feed->addAuthor(array('name' => $principal->GetProperty('displayname'), 'email' => $principal->GetProperty('email'), 'uri' => $c->protocol_server_port . $principal->url())); $feed_description = $collection->GetProperty('description'); if (isset($feed_description) && $feed_description != '') { $feed->setDescription($feed_description); } require_once 'RRule-v2.php'; $need_zones = array(); $timezones = array(); while ($event = $qry->Fetch()) { if ($event->caldav_type != 'VEVENT' && $event->caldav_type != 'VTODO' && $event->caldav_type != 'VJOURNAL') { dbg_error_log('feed', 'Skipping peculiar "%s" component in VCALENDAR', $event->caldav_type); continue; } $is_todo = $event->caldav_type == 'VTODO'; $ical = new vComponent($event->caldav_data); $event_data = $ical->GetComponents('VTIMEZONE', false); $item = $feed->createEntry(); $item->setId($c->protocol_server_port_script . ConstructURL($event->dav_name)); $dt_created = new RepeatRuleDateTime($event->created); $item->setDateCreated($dt_created->epoch()); $dt_modified = new RepeatRuleDateTime($event->modified); $item->setDateModified($dt_modified->epoch()); $summary = $event->summary; $p_title = $summary != '' ? $summary : translate('No summary'); if ($is_todo) { $p_title = "TODO: " . $p_title; } $item->setTitle($p_title); $content = ""; $dt_start = new RepeatRuleDateTime($event->dtstart); if ($dt_start != null) { $p_time = '<strong>' . translate('Time') . ':</strong> ' . strftime(translate('%F %T'), $dt_start->epoch()); $dt_end = new RepeatRuleDateTime($event->dtend); if ($dt_end != null) { $p_time .= ' - ' . ($dt_end->AsDate() == $dt_start->AsDate() ? strftime(translate('%T'), $dt_end->epoch()) : strftime(translate('%F %T'), $dt_end->epoch())); } $content .= $p_time; } $p_location = $event_data[0]->GetProperty('LOCATION'); if ($p_location != null) { $content .= '<br />' . '<strong>' . translate('Location') . '</strong>: ' . hyperlink($p_location->Value()); } $p_attach = $event_data[0]->GetProperty('ATTACH'); if ($p_attach != null) { $content .= '<br />' . '<strong>' . translate('Attachment') . '</strong>: ' . hyperlink($p_attach->Value()); } $p_url = $event_data[0]->GetProperty('URL'); if ($p_url != null) { $content .= '<br />' . '<strong>' . translate('URL') . '</strong>: ' . hyperlink($p_url->Value()); } $p_cat = $event_data[0]->GetProperty('CATEGORIES'); if ($p_cat != null) { $content .= '<br />' . '<strong>' . translate('Categories') . '</strong>: ' . $p_cat->Value(); $categories = explode(',', $p_cat->Value()); foreach ($categories as $category) { $item->addCategory(array('term' => trim($category))); } } $p_description = $event->description; if ($p_description != '') { $content .= '<br />' . '<br />' . '<strong>' . translate('Description') . '</strong>:<br />' . nl2br(hyperlink($p_description)); $item->setDescription($p_description); } $item->setContent($content); $feed->addEntry($item); //break; } $last_modified = new RepeatRuleDateTime($collection->GetProperty('modified')); $feed->setDateModified($last_modified->epoch()); $response = $feed->export('atom'); $cache->set($cache_ns, $cache_key, $response); return $response; }
/** * Find the collection associated with this resource. */ protected function FetchCollection() { global $session; /** * RFC4918, 8.3: Identifiers for collections SHOULD end in '/' * - also discussed at more length in 5.2 * * So we 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" */ dbg_error_log('DAVResource', ':FetchCollection: Looking for collection for "%s".', $this->dav_name); // Try and pull the answer out of a hat $cache = getCacheInstance(); $cache_ns = 'collection-' . preg_replace('{/[^/]*$}', '/', $this->dav_name); $cache_key = 'dav_resource' . $session->user_no; $this->collection = $cache->get($cache_ns, $cache_key); if ($this->collection === false) { $this->ReadCollectionFromDatabase(); if ($this->collection->type != 'principal') { $cache_ns = 'collection-' . $this->collection->dav_name; @dbg_error_log('Cache', ':FetchCollection: Setting cache ns "%s" key "%s". Type: %s', $cache_ns, $cache_key, $this->collection->type); $cache->set($cache_ns, $cache_key, $this->collection); } @dbg_error_log('DAVResource', ':FetchCollection: Found collection named "%s" of type "%s".', $this->collection->dav_name, $this->collection->type); } else { @dbg_error_log('Cache', ':FetchCollection: Got cache ns "%s" key "%s". Type: %s', $cache_ns, $cache_key, $this->collection->type); if (preg_match('#^(/[^/]+)/?$#', $this->dav_name, $matches) || preg_match('#^((/principals/[^/]+/)[^/]+)/?$#', $this->dav_name, $matches)) { $this->_is_principal = true; $this->FetchPrincipal(); $this->collection->is_principal = true; $this->collection->type = 'principal'; } @dbg_error_log('DAVResource', ':FetchCollection: Read cached collection named "%s" of type "%s".', $this->collection->dav_name, $this->collection->type); } if (isset($this->collection->bound_from)) { $this->_is_binding = true; $this->bound_from = str_replace($this->collection->bound_to, $this->collection->bound_from, $this->dav_name); if (isset($this->collection->access_ticket_id)) { if (!isset($this->tickets)) { $this->tickets = array(); } $this->tickets[] = new DAVTicket($this->collection->access_ticket_id); } } $this->_is_collection = $this->_is_principal || $this->collection->dav_name == $this->dav_name || $this->collection->dav_name == $this->dav_name . '/'; if ($this->_is_collection) { $this->dav_name = $this->collection->dav_name; $this->resource_id = $this->collection->collection_id; $this->_is_calendar = $this->collection->type == 'calendar'; $this->_is_addressbook = $this->collection->type == 'addressbook'; $this->contenttype = 'httpd/unix-directory'; if (!isset($this->exists) && isset($this->collection->exists)) { // If this seems peculiar it's because we only set it to false above... $this->exists = $this->collection->exists; } if ($this->exists) { if (isset($this->collection->dav_etag)) { $this->unique_tag = '"' . $this->collection->dav_etag . '"'; } if (isset($this->collection->created)) { $this->created = $this->collection->created; } if (isset($this->collection->modified)) { $this->modified = $this->collection->modified; } if (isset($this->collection->dav_displayname)) { $this->collection->displayname = $this->collection->dav_displayname; } } else { if (!isset($this->parent)) { $this->GetParentContainer(); } $this->user_no = $this->parent->GetProperty('user_no'); } if (isset($this->collection->resourcetypes)) { $this->resourcetypes = $this->collection->resourcetypes; } else { $this->resourcetypes = '<DAV::collection/>'; if ($this->_is_principal) { $this->resourcetypes .= '<DAV::principal/>'; } if ($this->_is_addressbook) { $this->resourcetypes .= '<urn:ietf:params:xml:ns:carddav:addressbook/>'; } if ($this->_is_calendar) { $this->resourcetypes .= '<urn:ietf:params:xml:ns:caldav:calendar/>'; } } } }
/** * 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; }