/**
     * 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);
            }
        }
    }
Exemple #2
0
    $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!
}
Exemple #4
0
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;
}
Exemple #5
0
 /**
  * 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;
 }