• Publicity: Public Only All

caldav-procs.tcl

CalDav implementation for OpenACS

Location:
packages/caldav/tcl/caldav-procs.tcl
Created:
Jan, 2017
Authors:
Gustaf Neumann
marmoser@wu.ac.at

Procedures in this file

Detailed information

caldav::get_public_calendars (private)

 caldav::get_public_calendars

Get all public calendars, which might return empty.

Partial Call Graph (max 5 caller/called nodes):
%3 parameter::get_from_package_key parameter::get_from_package_key (public) caldav::get_public_calendars caldav::get_public_calendars caldav::get_public_calendars->parameter::get_from_package_key

Testcases:
No testcase defined.

caldav::get_sync_calendar (private)

 caldav::get_sync_calendar -user_id user_id

Get the calendar, to which we want to sync. This method returns the first (and only in usual installations) calendar_id if it is found.

Switches:
-user_id (required)

Partial Call Graph (max 5 caller/called nodes):
%3 test_get_calitems get_calitems (test caldav) caldav::get_sync_calendar caldav::get_sync_calendar test_get_calitems->caldav::get_sync_calendar caldav::CalDAV instproc PUT caldav::CalDAV instproc PUT (public) caldav::CalDAV instproc PUT->caldav::get_sync_calendar caldav::test::get_lowest_uid caldav::test::get_lowest_uid (private) caldav::test::get_lowest_uid->caldav::get_sync_calendar

Testcases:
get_calitems
[ hide source ] | [ make this the default ]

Content File Source

ad_library {
    CalDav implementation for OpenACS
    @author Gustaf Neumann
    @author marmoser@wu.ac.at
    @creation-date Jan, 2017
}

ns_logctl severity Debug(caldav) true
ns_logctl severity Debug(caldav-request) true


# TODOS
# - DELETE, PUT
#
# - move get_sync_calendar to interface-procs
#
# - There is a lot of guess work in cases, when no calendar UID is
#   set (using the activity_id in such cases). Not sure, if it would
#   be better to set UID during exports, such we have always valid
#   UIDs for calendar items.
#
# - etags on modification date are not good enough.
#
# - The implementation depends on NaviServer by using
#   e.g. "ns_cache_eval", "ns:memoize" and "ns_conn
#   partialtimes". with some effort, it would possible to provide an
#   AOLserver version as well.
#

::xo::library require caldav-interface-procs

namespace eval ::caldav {

    d_proc -private get_sync_calendar {
        -user_id:required
    } {

        Get the calendar, to which we want to sync.  This method
        returns the first (and only in usual installations)
        calendar_id if it is found.

    } {
        set calendar_id [ns_cache_eval ns:memoize caldav-sync-cal-$user_id \
                             ::xo::dc list get_sync_calendar {
                                 select calendar_id from calendars
                                 where private_p = 't' and owner_id = :user_id
                             }]
        if {[llength $calendar_id] > 1} {
            #
            # In general, there is no rule which states that there is
            # for a user just a single private calendar. However, in
            # typical installations, this is the case for "normal"
            # users. In case there are multiple such calendars, return
            # the first a sync calendar and issue a warning. Probably,
            # such cases should be fixed manually in the database.
            #
            ns_log warning "CalDAV: more than one sync calendar for user $user_id ($calendar_id);" \
                "fall back to first one"
            set calendar_id [lindex $calendar_id 0]
        } elseif {[llength $calendar_id] == 0} {
            #
            # On non-DotLRN installations, the private calendar is
            # created when a user visits the first time the calendar
            # package, and no personal calendar exists. Bail out if
            # no such calendar exists.
            #
            error "can't get sync calendar for user $user_id"
        }
        return $calendar_id
    }

    d_proc -private get_public_calendars {
    } {
        Get all public calendars, which might return empty.
    } {
        #
        # In case we have a dotlrn installation,
        #
        if {[info commands ::dotlrn_calendar::my_package_key] ne ""} {
            set main_calendar_package_id [parameter::get_from_package_key \
                                              -package_key [dotlrn_calendar::my_package_key] \
                                              -parameter main_calendar_package_id]

            set calendar_ids [ns_cache_eval ns:memoize caldav-cal-public \
                                  ::xo::dc list -prepare integer cal {
                                      select calendar_id from calendars
                                      where package_id = :main_calendar_package_id
                                      and private_p = 'f'
                                  }]
        } else {
            set calendar_ids {}
        }
        return $calendar_ids
    }


    xotcl::Class create CalDAV -superclass ::xo::ProtocolHandler -parameter {
        {url /caldav/}
    }

    CalDAV instproc debug {msg} {
        ns_log Debug(caldav) "[uplevel self proc]: $msg"
    }


    #
    # The two methods "getcontent" and "response" are responsible for
    # consistent logging. "getcontent" should be called on the begin
    # of every HTTP method implementation, "response" should be used
    # for delivering the result.
    #

    CalDAV instproc response {code mimetype response} {
        ns_log Debug(caldav-request) "Response ([ns_conn partialtimes]) for ${:method} $code ${:uri}\n$response"
        ns_return $code $mimetype $response
    }

    CalDAV instproc getcontent {{headers ""}} {
        lappend headers Content-Type ""  User-Agent ""
        foreach {tag default} $headers {
            lappend reportHeaders [list $tag [ns_set iget [ns_conn headers] $tag $default]]
        }
        set content [ns_getcontent -as_file false -binary false]
        set msg "[ns_conn method] ${:uri} ([join $reportHeaders {, }])"
        if {$content ne ""} {
            append msg ":\n$content"
        }
        ns_log Debug(caldav-request) $msg
        return $content
    }

    CalDAV instproc get_uid_from_href {href} {
        set uid ""
        regexp {/([^/]+)[.]ics$} $href . uid
        return $uid
    }


    ###
    ###  HTTP method GET
    ###
    CalDAV ad_instproc GET {} {

        GET calendar content in ical syntax. The method can either
        return an individual calendar item based on a cal_uid or a
        full calendar (may be an aggregated calendar) depending on the
        syntax of the URL.

        - individual cal_items (must end with .ics)

        /caldav/calendar/12872/12907.ics

        -  complete calendar

        /caldav/calendar
        /caldav/calendar?calendar_ids=12872
        /caldav/calendar/12872

        where the "12872" is a calendar_id and "12907"
        is the UID of a calendar item.

    } {
        # the following getcontent call is just for consistent logging
        :getcontent
        #
        #
        set tail [lindex ${:urlv} end]
        set mimetype "text/calendar"
        set code 404
        set resp ""
        :debug ":urlv [list ${:urlv}]"

        if {[file extension $tail] eq ".ics"} {
            #
            # Retrieve a single item identified by an UID
            #
            set uid [:get_uid_from_href ${:uri}]
            :debug "return single calendar item for uid $uid"

            #
            # We need the calendar_ids for consistent naming of
            # calendars (aggregated, etc.)....
            #
            if {[string is integer -strict [lindex ${:urlv} end-1]]} {
                set calendar_ids [lindex ${:urlv} end-1]
            } else {
                set calendar_ids ""
            }
            :calendar_ids $calendar_ids

            #
            # GN TODO: running two queries is not optimal but having
            # these separate eases type mismatch.
            #
            #    cal_uids(cal_uid, on_which_activity references acs_activities(activity_id), ical_vars)
            #    cal_items(cal_item_id references acs_events(event_id), on_which_calendar, ...)
            #    acs_events(event_id, ..., activity_id references acs_activities(activity_id), ...)
            #
            set cal_items [::xo::dc list_of_lists -prepare varchar get_calitem {
                select e.event_id as cal_item_id, u.ical_vars, a.creation_date, a.last_modified
                from   cal_uids u, acs_events e, acs_objects a
                where  u.cal_uid = :uid
                and    e.activity_id = u.on_which_activity
                and    a.object_id = e.event_id
                limit 1
            }]
            :debug "cal_items 1 (join on <$uid>): <$cal_items>"
            if {[llength $cal_items] == 0 && [string is integer -strict $uid]} {
                set cal_items [::xo::dc list_of_lists -prepare integer get_calitem {
                    select e.event_id as cal_item_id, '' as ical_vars, a.creation_date, a.last_modified
                    from   acs_events e, acs_objects a
                    where  e.activity_id = :uid
                    and    a.object_id = e.event_id
                    limit 1
                }]
            }

            #:debug "cal_items 2: <$cal_items>"
            if {[llength $cal_items] == 1} {
                lassign [lindex $cal_items 0] cal_item_id ical_vars creation_date last_modified
                calendar::item::get -cal_item_id $cal_item_id -array c
                :debug "calendar::item::get -cal_item_id $cal_item_id->\n[array get c]"
                set vevent [calitem new  \
                                -uid $uid \
                                -creation_date $creation_date \
                                -last_modified $last_modified \
                                -dtstart $c(start_date_ansi) \
                                -is_day_item [dt_no_time_p -start_time $c(start_date_ansi) -end_time $c(end_date_ansi)] \
                                -formatted_recurrences [calendars format_recurrence -recurrence_id $c(recurrence_id)] \
                                -dtend $c(end_date_ansi) \
                                -ical_vars $ical_vars \
                                -location $c(location) \
                                -summary $c(name) \
                                -description $c(description)]
                unset -nocomplain c
                append resp [$vevent as_ical_calendar -calendar_name [:calendarName]]
                $vevent destroy

                # GN TODO: do we need always the database to get an etag ?
                ns_set put [ns_conn outputheaders] ETag [subst {"[:getETagByUID $uid]"}]
                set code 200
            } else {
                #
                # Nothing found or a weird result (multiple items). If
                # nothing is found, we have the error status code from
                # above. On too many results, raise an exception.
                #
                if {[llength $cal_items] > 1} {
                    error "cal_item query based on cal_uid $uid lead to multiple results [llength $cal_items]"
                }
            }
        } else {
            #
            # Retrieve a calendar.
            #
            # This query is run currently without a time constraints.
            #
            # GN TODO: not sure, returning always the full calendar is
            # a good idea, my personal calendar has more than 10 years
            # of data ... but maybe this is not often executed.
            #
            # The query parameter "calendar_ids" is used e.g. for
            # regression testing.
            #
            # GN TODO:
            #
            if {[llength ${:urlv}] > 1} {
                set calendar_ids [lindex ${:urlv} 1]
            } else {
                set calendar_ids [ns_queryget calendar_ids ""]
            }
            :calendar_ids $calendar_ids

            if {$calendar_ids ne ""} {
                #
                # For specified calendars, return the calendar name of
                # the first calendar_id
                #
                set calendar_query "-calendar_ids $calendar_ids"
            } else {
                set calendar_query ""
            }
            set resp [calendars header -calendar_name [:calendarName]]
            foreach item [calendars get_calitems -user_id ${:user_id} {*}$calendar_query] {
                append resp [$item as_ical_event]
            }
            append resp [calendars footer]
            set code 200
        }
        :response $code $mimetype $resp

    }

    CalDAV instproc request_error {msg} {
        ad_log warning "CalDAV: $msg"
        :response 500 text/plain ""
    }

    ###
    # PROPFIND Method
    ###
    CalDAV ad_instproc PROPFIND {} {

        read and answer PROPFIND requests
        RFC 2518, section 8.1
        https://tools.ietf.org/html/rfc4918#page-35

    } {

        #https://github.com/seiyanuta/busybook/blob/master/Documentation/caldav-request-examples.md
        # Do a PROPFIND on the url the user supplied, requesting {DAV:}current-user-principal.
        # Using this url, you do a PROPFIND to find out more information about the user.
        # Here, you should typically request for the calendar-home-set property in the caldav namespace.
        # Then, using the calendar-home-set, do a PROPFIND (depth: 1) to find the calendars.


        # A client MUST submit a Depth header with a value of "0", "1",
        # or "infinity" with a PROPFIND request.  Servers MUST support
        # "0" and "1" depth requests on WebDAV-compliant resources and
        # SHOULD support "infinity" requests.  Servers SHOULD treat a
        # request without a Depth header as if a "Depth: infinity"
        # header was included.
        #

        set depth [ns_set iget [ns_conn headers] Depth "infinity"]
        #ns_log notice "caldav: depth = $depth"

        set content [:getcontent {Depth "infinity"}]
        set doc [:parseRequest $content]
        ns_log notice "after parseRequest <$content>"
        if {$doc eq ""} {
            return [:request_error "could not parse request. Probably invalid XML"]
        }

        set root [$doc documentElement]
        #
        # Element name of root node must be "propfind"
        #
        if {[$root localName] ne "propfind"} {
            $doc delete
            return [:request_error "invalid request, no <propfind> element"]
        }

        set prop [$root firstChild]
        #
        # child of root must be prop or allprop
        #
        set elementName [$prop localName]
        if {$elementName ni {prop allprop}} {
            $doc delete
            return [:request_error "invalid request, no <prop> or <allprop> element, but '$elementName' provided"]
        }

        #
        # Special case allprop: return all properties
        #
        if {$elementName eq "allprop"} {
            dom parse -- {
                <A:prop xmlns:A="DAV:">
                <A:getcontenttype/>
                <A:getetag/>
                <A:sync-token/>
                <A:supported-report-set/>
                <c:getctag xmlns:c="http://calendarserver.org/ns/"/>
                <A:resourcetype/>
                <B:supported-calendar-component-set xmlns:B="urn:ietf:params:xml:ns:caldav"/>
                <B:calendar-home-set xmlns:B="urn:ietf:params:xml:ns:caldav"/>
                <A:displayname/>
                <A:current-user-principal/>
                </A:prop>
            } propTree
            set prop [$propTree firstChild]
        }

        set response ""

        # The incoming request URI should fall into the following categories
        #
        # - TOPLEVEL:      /caldav
        # - PRINCIPAL:     /caldav/principal
        # - FULL CALENDAR: /caldav/calendar
        #

        # GN TODO: remove the following test when we are sure it is ok.
        #ns_log notice "PROPFIND <${:uri}>, tail <[file tail ${:uri}]> urlv: <${:urlv}>"
        if {[file tail ${:uri}] ne [lindex ${:urlv} end]} {
            ns_log notice "========================== <[file tail ${:uri}]> ne <[lindex ${:urlv} end]>"
            error
        }

        switch [lindex ${:urlv} end] {
            "" {
                ns_log notice "=== CalDAV /caldav/ depth $depth"
                append response [:generateResponse -queryType top -user_id ${:user_id} $prop]
                if {$depth ne "0"} {
                    #
                    # Not sure, what should be sent in such cases. we
                    # could send "principal" or "calendar" replies.

                    ns_log notice "CalDAV /caldav/ query <${:uri}> with depth $depth"
                    #append response [:generateResponse -queryType principal $prop]
                }
            }
            "principal" {
                # A principal is a network resource that represents a distinct human or
                # computational actor that initiates access to network resources.
                # https://tools.ietf.org/html/rfc3744
                append response [:generateResponse -queryType principal $prop]
                #
                # here we ignore depth for the time being
                #
            }
            "calendar" {
                # TODO: we assume, we are working on the aggregated calendar
                :calendar_ids ""

                #set calendar_id [::caldav::get_sync_calendar -user_id ${:user_id}]
                #append response [:generateResponse -queryType calendar -calendar_id $calendar_id $prop]
                append response [:generateResponse -queryType calendar -calendar_id "" $prop]
                #ns_log notice "=== CALENDAR depth $depth FIND getetag=[$prop selectNodes -namespaces {d DAV:} //d:getetag] PROP [$prop asXML] "

                if {$depth ne "0"} {
                    #
                    # Search in depth 1 to deeper. Since all our
                    # calendar data is at depth 1, there is no need to
                    # differentiate.
                    #
                    # GN TODO: In general, PROPFIND on calendars is
                    # needed also without etags, not sure, where the
                    # restriction concerning "etag"comes from.
                    #
                    if {[$prop selectNodes -namespaces {d DAV:} //d:getetag] ne ""} {
                        #
                        # Query attributes containing also etags for
                        # this users' resources
                        #
                        foreach item [calendars get_calitems -user_id ${:user_id}] {
                            append response [:generateResponse -queryType resource -cal_item $item $prop]
                        }
                    } else {
                        ns_log notice "CalDAV: ===== calendar query on components with depth $depth on [$prop asXML]"
                    }
                }
            }
            default {
                $doc delete
                ad_log warning "caldav: PROFIND called for unknown resource: '${:uri}'"
                :response 404 text/plain "no such resource: '${:uri}'"
                return
                #return [:request_error "invalid URI '${:uri}'"]
            }
        }
        # create response
        append resp \
            {<?xml version="1.0" encoding="utf-8" ?>} \n\
            {<d:multistatus } \
            {xmlns:d="DAV:" } \
            {xmlns:cs="http://calendarserver.org/ns/" } \
            {xmlns:c="urn:ietf:params:xml:ns:caldav" } \
            {xmlns:ical="http://apple.com/ns/ical/">} \
            $response \
            </d:multistatus>

        :response 207 text/xml $resp
        $doc delete
    }

    CalDAV instproc calendar_ids {value} {
        #
        # When calendar_ids is "", we work on on aggregated
        # (dotlrn-style calendar), when nonempty (single or multiple
        # calendars) we work on concrete calendars
        #
        set :calendar_ids $value
    }

    CalDAV instproc calendarName {} {
        :debug "calendarName has :calendar_ids <${:calendar_ids}>"
        if {${:calendar_ids} eq ""} {
            set calendar_name [:aggregatedCalendarName]
        } else {
            calendar::get -calendar_id [lindex ${:calendar_ids} 0] -array calinfo
            #:debug "calinfo of [lindex ${:calendar_ids} 0]: [array get calinfo]"
            set calendar_name $calinfo(calendar_name)
        }
        return $calendar_name
    }

    CalDAV instproc aggregatedCalendarName {} {
        set d [site_node::get_from_object_id -object_id [ad_conn subsite_id]]
        set instance_name [lang::util::localize [dict get $d instance_name]]
        set user_name [::xo::get_user_name ${:user_id}]
        return [_ caldav.aggregated_calendar_name \
                    [list instance_name $instance_name user_name $user_name]]
    }

    #################################################################################
    # FINDPROP property handlers
    #################################################################################
    CalDAV instproc unknown args {
        ns_log notice "CalDAV ${:method} unknown <$args>"
        return ""
    }

    #
    # Properties from xmlns "DAV:"
    #
    CalDAV instproc property=d-current-user-principal {res node} {
        #
        # Indicates a URL for the currently authenticated user's
        # principal resource on the server.
        #
        return <d:href>${:url}principal</d:href>
    }

    CalDAV instproc property=d-current-user-privilege-set {res node} {
        return {
            <d:privilege><d:all /></d:privilege>
            <d:privilege><d:read /></d:privilege>
            <d:privilege><d:write /></d:privilege>
            <d:privilege><d:write-properties /></d:privilege>
            <d:privilege><d:write-content /></d:privilege>
        }
    }

    CalDAV instproc property=d-displayname {res node} {
        #
        # The displayname property should be defined on all DAV
        # compliant resources. If present, it provides a name for the
        # resource that is suitable for presentation to a user.
        #
        # https://tools.ietf.org/html/rfc2518#section-13.2
        #
        switch ${:queryType} {
            "calendar"  { return [ns_quotehtml [:aggregatedCalendarName]] }
            "resource"  { return [ns_quotehtml [$res cget -summary]] }
            "principal" { return [ns_quotehtml [::xo::get_user_name ${:user_id}]]}
        }
        return ""
    }

    CalDAV instproc property=d-getcontenttype {res node} {
        #
        # Contains the Content-Type header returned by a GET without
        # accept headers.
        #
        return "text/calendar; charset=utf-8"
    }

    CalDAV instproc property=d-getetag {res node} {
        # An etag is the entity-id of the resource
        if {${:queryType} eq "resource"} {
            # cal_items have already etags precalculated
            return [$res cget -etag]
        }
        # Other resource types (calendars, principal, user) do not have etags
        return ""
    }

    CalDAV instproc property=d-owner {res node} {
        # This  property identifies a particular principal as being the "owner"
        # of the resource.
        #return "<d:href>${:url}</d:href>"
        return ""
    }

    CalDAV instproc property=d-principal-address {res node} {
        return <d:href>${:url}principal</d:href>
    }
    CalDAV instproc property=d-principal-collection-set {res node} {
        return [subst {<d:href>${:url}principal</d:href>}]
    }
    CalDAV instproc property=d-principal-URL {res node} {
        #
        # A principal may have many URLs, but there must be one
        # "principal URL" that clients can use to uniquely identify a
        # principal.  This protected property contains the URL that
        # MUST be used to identify this principal in an ACL request.
        #
        return <d:href>${:url}principal</d:href>
    }


    CalDAV instproc property=d-supported-report-set {res node} {
        return {
            <d:supported-report><d:report><c:calendar-multiget/></d:report></d:supported-report>
            <d:supported-report><d:report><c:calendar-query/></d:report></d:supported-report>
        }
    }

    CalDAV instproc property=d-sync-token {res node} {
        # set result [:calcSyncToken ${:user_id}]
        return ""
    }

    CalDAV instproc property=d-resourcetype {res node} {
        # Specifies the nature of the resource.
        # todo:resourcetype for single item
        switch ${:queryType} {
            "resource" {
                # single items do not have a resourcetype
                return ""
            }
            "calendar" {
                return <d:collection/><c:calendar/>
            }
            "principal" -
            "default" {
                return <d:collection/>
            }
        }
    }

    #
    # Properties from xmlns "http://calendarserver.org/ns/"
    #
    CalDAV instproc property=cs-getctag {res node} {
        # Specifies a "synchronization" token used to indicate when
        # the contents of a calendar or scheduling Inbox or Outbox
        # collection have changed.
        if {${:queryType} eq "calendar"} {
            return [:calcCtag ${:user_id}]
        } else {
            return ""
        }
    }

    #
    # Properties from xmlns "urn:ietf:params:xml:ns:caldav"
    # https://tools.ietf.org/html/rfc4791
    #

    CalDAV instproc property=c-calendar-data {res node} {
        #
        # RFC 4791: Given that XML parsers normalize the two character
        # sequence CRLF (US-ASCII decimal 13 and US-ASCII decimal 10)
        # to a single LF character (US-ASCII decimal 10), the CR
        # character (US-ASCII decimal 13) MAY be omitted in calendar
        # object resources specified in the CALDAV:calendar-data XML
        # element.  Therefore, we do not have to do special encoding
        # on the CR.
        #
        # https://tools.ietf.org/html/rfc4791#page-79
        #
        # That being said, the content lines of iCalendar objects specified in
        # the request body of a PUT or in the response body of a GET will still
        # be required to be delimited by a CRLF sequence (US-ASCII decimal 13,
        # followed by US-ASCII decimal 10).
        #
        # https://www.ietf.org/mail-archive/web/caldav/current/msg00551.html

        if {${:queryType} eq "resource"} {
            return [ns_quotehtml [$res as_ical_calendar]]
        } else {
            ns_log warning "CalDav can't return c-calendar-data for ${:queryType}"
        }
    }

    CalDAV instproc property=c-calendar-home-set {res node} {
        return <d:href>${:url}calendar</d:href>
    }
    CalDAV instproc property=c-supported-calendar-component-set {res node} {
        #
        # Specifies the calendar component types (e.g., VEVENT,VTODO,
        # VJOURNAL etc.)  that calendar object resources can contain
        # in the calendar collection.  c:comp name="VTODO" />
        #
        return "<c:comp name='VEVENT'/>"
    }

    CalDAV instproc property=c-calendar-description {res node} {
        return [ns_quotehtml [:aggregatedCalendarName]]
    }

    CalDAV instproc property=c-calendar-user-address-set {res node} {
        #
        # Identify the calendar addresses of the associated principal resource.
        # return <d:href>${:url}</d:href>
        return ""
    }

    CalDAV instproc property=c-calendar-timezone {res node} {
        #return [calendars timezone]
        return ""
    }


    #
    # Properties from xmlns "http://apple.com/ns/ical/"
    #
    CalDAV instproc property=ical-calendar-color {res node} {
        if {${:queryType} eq "calendar"} {
            # GN TODO:do not hard-code colors
            return "#2C5885"
        }
        return ""
    }
    CalDAV instproc property=ical-calendar-order {res node} {
        return 1
    }


    CalDAV ad_instproc returnXMLelement {resource node} {
        Return a pair of values indicating success and the value to
        be returned. While the property=* methods above return just
        values, this method wraps it into XML elements for returning
        it.
    } {
        #
        #
        set property [$node localName]
        set ns       [$node name]
        :debug "<$property> <$ns> resource $resource queryType <${:queryType}>"

        if {[info exists :xmlns($ns)]} {
            set outputNs [set :xmlns($ns)]
            set methodName property=$outputNs-$property
            #:debug "call method $methodName"
            try {
                set value [:$methodName $resource $node]
                :debug "<$property> <$ns> resource $resource queryType <${:queryType}> -> '$value'"
            } on error {errorMsg} {
                ns_log warning "CalDAV returnXMLelement: call method $methodName raised exception: $errorMsg"
                set value ""
            }
            if {$value eq ""} {
                set result [list 1 <${outputNs}:$property/>]
                #:debug "${:method} query $methodName returns empty (probably not found)"
            } else {
                set result [list 0 <${outputNs}:$property>$value</${outputNs}:$property>]
            }
        } else {
            ns_log warning "CalDAV: client requested a element with unknown namespace $ns known ([array names :xmlns])"
            set result [list 1 [subst {<[$node nodeName] xmlns:[$node prefix]="[$node namespaceURI]"/>}]]
        }
        return $result
    }

    CalDAV ad_instproc generateResponse {
        -queryType:required
        -user_id
        -calendar_id
        -cal_item
        node
    } {

        Return a &lt;response&gt; ... &lt;/response&gt; entry for the
        URL specified in the ${:url} and for the query attributes
        specified in the tdom node. The attributes user_id,
        calendar_id, or cal_item have to be set according to the
        queryType.

        @param queryType is an abstraction of the query url and
        can be "calendar""resource""top", or "principal"

    } {
        # @prop: requested properties as tdom nodes
        # generate xml for this resource

        #ns_log notice "generateResponse $queryType"
        set :queryType $queryType
        switch $queryType {
            "resource" {
                set href "${:url}calendar/[ns_urlencode [$cal_item cget -uid]].ics"
                set resource $cal_item
            }
            "calendar" {
                set href "${:url}calendar/"
                set resource $calendar_id
            }
            "top" {
                set href ${:url}
                set resource $user_id
            }
            "principal" {
                set href "${:url}principal"
                set resource "none"
            }
            "default" {
                error "invalid input"
            }
        }
        ns_log notice "generateResponse href=$href, resource=$resource child nodes [llength [$node childNodes]]"
        set not_found_props ""
        set found_props ""
        foreach childNode [$node childNodes] {
            lassign [:returnXMLelement $resource $childNode] returncode val
            if {$returncode} {
                append not_found_props $val \n
            } else {
                append found_props $val \n
            }
        }
        :debug "found_props $found_props, not_found_props $not_found_props"
        append result \
            <d:response> \n \
            <d:href>$href</d:href> \n

        if {$found_props ne ""} {
            #
            # return 200 for properties that were found
            #
            append result \
                <d:propstat> \n \
                <d:prop>$found_props</d:prop> \n \
                "<d:status>HTTP/1.1 200 OK</d:status>" \n \
                </d:propstat>
        }

        if {$not_found_props ne ""} {
            #
            # return 404 for properties that were not found
            #
            append result \n\
                <d:propstat> \n\
                <d:prop>$not_found_props</d:prop> \n\
                "<d:status>HTTP/1.1 404 Not Found</d:status>" \n\
                </d:propstat>
        }
        append result \n </d:response>
        return $result
    }

    ###
    ### HTTP method PROPPATCH
    ###
    CalDAV instproc PROPPATCH {} {

        set content [:getcontent]
        set doc [:parseRequest $content]

        # TODO: we assume, we are working on the aggregated calendar
        :calendar_ids ""

        if {$doc eq ""} {
            return [:request_error "request document invalid:\n$content"]
        }
        set reply ""
        set innerresponse ""

        set root [$doc documentElement]
        set props [$root selectNodes -namespaces ${:namespaces} /d:propertyupdate/d:set/d:prop]
        if {[llength $props] == 0} {
            ns_log Warning "PROPPATCH: invalid request: no property /d:propertyupdate/d:set/d:prop in\n$content"
            set statusCode 400
        } else {
            #
            # Return a multistatus with all properties forbidden.
            #
            foreach n [$props childNodes] {
                append innerresponse [subst {<d:propstat>
                    <d:prop><Z:[$n localName] xmlns:Z="[$n name]"/></d:prop>
                    <d:status>HTTP/1.1 403 Forbidden</d:status>
                    </d:propstat>}]
            }
            set resp [subst {<?xml version="1.0" encoding="utf-8" ?>
                <d:multistatus xmlns:d="DAV:">
                <d:response>
                <d:href>[string trimright [ns_conn url] "/"]</d:href>
                $innerresponse
                </d:response>
                </d:multistatus>}]
            set statusCode 207
        }
        :response $statusCode text/xml $resp
        $doc delete
    }

    ###
    ### REPORT
    ###
    CalDAV ad_instproc REPORT {} {

        CalDAV REPORT Method, see RFC 3253, section 3.6

    } {

        set content [:getcontent]
        set doc [:parseRequest $content]

        if {$doc eq ""} {
            return [:request_error "empty reports are not allowed"]
        }

        #
        # Currently, three reports are supported
        #
        # - calendar-multiget
        # - calendar-query
        # - sync-collection
        #
        $doc documentElement root
        set responses_xml ""

        # TODO: we assume, we are working on the aggregated calendar
        :calendar_ids ""

        if {[$root selectNodes -namespaces {c urn:ietf:params:xml:ns:caldav} "//c:calendar-multiget"] ne ""} {
            set ics_set [:calendar-multiget [$root firstChild]]

        } elseif {[$root selectNodes -namespaces {c urn:ietf:params:xml:ns:caldav} "//c:calendar-query"] ne ""} {
            set ics_set [:calendar-query $root]

        } elseif {[$root selectNodes -namespaces {d DAV:} "//d:sync-collection"] ne ""} {
            set ics_set [:sync-collection $root responses_xml]

        } else {
            #unknown type requested, aborting
            $doc delete
            return [:request_error "request type unknown [$root localName]"]
        }

        set props [$root selectNodes -namespaces {d DAV:} "d:prop"]
        foreach ics $ics_set {
            append responses_xml [:generateResponse -queryType resource -cal_item $ics $props]
        }

        append xml \
            {<?xml version="1.0" encoding="utf-8"?>} \n\
            {<d:multistatus xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:cs="http://calendarserver.org/ns">} \n\
            $responses_xml \n\
            </d:multistatus>

        :response 207 text/xml $xml
        $doc delete
    }



    CalDAV ad_instproc calendar-multiget {prop} {

        calendar-multiget REPORT is used to retrieve specific calendar
        object resources from within a collection, if the Request- URI
        is a collection, or to retrieve a specific calendar object
        resource, if the Request-URI is a calendar object
        resource. This report is similar to the CALDAV:calendar-query
        REPORT (see Section 7.8), except that it takes a list of
        DAV:href elements, instead of a CALDAV:filter element, to
        determine which calendar object resources to return.

        https://icalendar.org/CalDAV-Access-RFC-4791/7-9-caldav-calendar-multiget-report.html
    } {
        #ns_log notice "==== calendar-multiget {$prop}"
        set cal_uids {}
        set act_ids {}

        foreach node [$prop selectNodes -namespaces {d DAV:} //d:href/text()] {
            set href [$node asText]
            set id [:get_uid_from_href $href]
            if {$id eq ""} {
                ns_log notice "CalDAV calendar-multiget: ignore href '$href'"
                continue
            }
            if {[string is integer -strict $id]
                && [::xo::dc get_value -prepare integer is_activity_id {
                    select 1 from acs_activities where activity_id = :id
                } 0] != 0
            } {
                lappend act_ids $id
            } else {
                lappend cal_uids [ns_urldecode $id]
            }
        }

        #
        # GN TODO: it is probably better to use a UNION instead of the
        # construct of "coalesce()" and "OR" below. We need to
        # test/benchmark with a full database with real-world data for
        # this.
        #
        set uid_clauses {}
        if {[llength $cal_uids] > 0} {
            lappend uid_clauses "u.cal_uid in ([ns_dbquotelist $cal_uids])"
        }
        if {[llength $act_ids] > 0} {
            lappend uid_clauses "e.activity_id in ([ns_dbquotelist $act_ids])"
        }
        ns_log notice uid_clauses=$uid_clauses

        set recurrences {}
        set ics_set {}

        #
        # TODO: why not [calendar get_calitems] ?
        #

        foreach row [::xo::dc list_of_lists get_calitems [subst {
            select
            md5(last_modified::text) as etag,
            coalesce(cal_uid, e.activity_id::varchar),
            ical_vars,
            on_which_calendar,
            c.item_type_id,
            to_char(start_date, 'YYYY-MM-DD HH24:MI:SS'),
            to_char(end_date, 'YYYY-MM-DD HH24:MI:SS'),
            coalesce(e.name, a.name),
            coalesce(e.description, a.description),
            c.cal_item_id,
            recurrence_id,
            creation_date,
            last_modified
            from
            acs_objects ao,
            acs_events e
            left outer join cal_uids u on u.on_which_activity = e.activity_id,
            acs_activities a,
            timespans s,
            time_intervals t,
            cal_items c
            where e.event_id = ao.object_id
            and a.activity_id = e.activity_id
            and c.cal_item_id = e.event_id
            and e.timespan_id = s.timespan_id
            and s.interval_id = t.interval_id
            and ( [join $uid_clauses " or "])
            order by start_date asc
        }]] {
            lassign $row etag cal_uid ical_vars calendar_id item_type \
                start_date end_date name description \
                cal_item_id recurrence_id creation_date last_modified

            if {$recurrence_id ne "" && $recurrence_id in $recurrences} {
                continue
            }
            # set item [::caldav::calitem new  \
                #               -id $cal_item_id \
                #               -uid $cal_uid \
                #               -ical_vars $ical_vars \
                #               -etag $etag \
                #               -calendar $calendar_id \
                #               -creation_date $creation_date \
                #               -last_modified $last_modified \
                #               -dtstart $start_date \
                #               -is_day_item [dt_no_time_p -start_time $start_date -end_time $end_date] \
                #               -formatted_recurrences [calendars format_recurrence -recurrence_id $recurrence_id] \
                #               -dtend $end_date \
                #               -summary $name \
                #               -description $description \
                #               -item_type $item_type]
            set item [::caldav::calitem new  \
                          -uid $cal_uid \
                          -ical_vars $ical_vars \
                          -etag $etag \
                          -creation_date $creation_date \
                          -last_modified $last_modified \
                          -dtstart $start_date \
                          -is_day_item [dt_no_time_p -start_time $start_date -end_time $end_date] \
                          -formatted_recurrences [calendars format_recurrence -recurrence_id $recurrence_id] \
                          -dtend $end_date \
                          -summary $name \
                          -description $description \
                         ]
            $item destroy_on_cleanup
            lappend ics_set $item
            lappend recurrences $recurrence_id
        }
        return $ics_set
    }

    CalDAV ad_instproc calendar-query {root} {
        Client wants the complete calendar
        Open: restrict to types
    } {
        set start ""
        set end ""
        #
        # determine filters
        #
        set time_range [$root selectNodes -namespaces {c urn:ietf:params:xml:ns:caldav} "//c:time-range"]
        #ns_log notice "time-range : $time_range"

        if {$time_range ne ""} {
            if {[$time_range hasAttribute start]} {
                set start [$time_range getAttribute start]
                set start [::xo::ical clock_to_oacstime [::xo::ical utc_to_clock $start]]
            }
            if {[$time_range hasAttribute end]} {
                set end [$time_range getAttribute end]
                set end [::xo::ical clock_to_oacstime [::xo::ical utc_to_clock $end]]
            }
        }
        ns_log notice "calendar-query: start time $start end time $end"

        #
        # components
        #
        set time_range [$root selectNodes -namespaces {c urn:ietf:params:xml:ns:caldav} "//c:comp-filter"]
        set fetch_vtodos 0
        set fetch_vevents 0
        foreach x $time_range {
            if {[$x getAttribute name] eq "VEVENT"} {set fetch_vevents 1}
            if {[$x getAttribute name] eq "VTODO"} {set fetch_vtodos 1}
        }
        set ics_set ""
        if {$fetch_vevents} {
            set ics_set [calendars get_calitems -user_id ${:user_id} -start_date $start -end_date $end]
        }
        return $ics_set
    }

    CalDAV ad_instproc sync-collection {root extraXMLvar} {
        sync
    } {
        upvar $extraXMLvar extraXML

        set props [$root selectNodes -namespaces {d DAV:} "//d:prop"]
        #set sync_level_node [$root selectNodes -namespaces {d DAV:} "//d:sync-level"]
        set sync_token_node [$root selectNodes -namespaces {d DAV:} "//d:sync-token"]
        if {$sync_token_node ne ""} {
            set sync_token [$sync_token_node text]
        } else {
            set sync_token ""
        }
        :debug "received sync-token <$sync_token>"

        #
        # Calculate a new sync token and return this as extraXML
        #
        set new_sync_token [:calcSyncToken ${:user_id}]
        set extraXML <d:sync-token>$new_sync_token</d:sync-token>

        if {$sync_token eq ""} {
            #
            # return all cal_items
            #
            set ics_set [calendars get_calitems -user_id ${:user_id}]

        } elseif {$sync_token ne $new_sync_token} {
            #
            # return cal_items since last sync_token
            #
            set ics_set [calendars get_calitems \
                             -user_id ${:user_id} \
                             -start_date [ns_fmttime $sync_token "%Y-%m-%d 00:00"] \
                             -end_date [ns_fmttime $new_sync_token "%Y-%m-%d 00:00"]]
        } else {
            #
            # return cal_items since the day of the sync_token
            #
            set ics_set [calendars get_calitems \
                             -user_id ${:user_id} \
                             -start_date [ns_fmttime $sync_token "%Y-%m-%d 00:00"]]
        }
        return $ics_set
    }

    ###
    ###  HTTP method OPTIONS
    ###
    CalDAV instproc OPTIONS {} {
        # A minimal definition for OPTIONS. Extend if necessary.
        set content [:getcontent]
        ns_set put [ns_conn outputheaders] DAV "1,2,access-control,calendar-access"
        ns_set put [ns_conn outputheaders] Allow "OPTIONS,GET,DELETE,PROPFIND,PUT,REPORT"
        ns_set put [ns_conn outputheaders] Cache-Control "no-cache"
        # return the response
        :response 200 "text/plain" {}
    }


    CalDAV ad_instproc calcCtag {user_id} {

        Generate a ctag (collection entity tag). The calendar ctag is
        like a resource etag; it changes when anything in the (joint)
        calendar has changed.

        This implementation returns a md5 value from the sum of the
        cal_item_ids, which changes when an item is deleted or a new
        community is joined, and the latest modification date of the
        cal_item_ids.

    } {
        lappend clauses \
            {*}[calendars communityCalendarClause $user_id] \
            {*}[calendars alwaysQueriedClause $user_id]
        #ns_log notice "calendar_clause [join $clauses { union }]"
        return [::xo::dc get_value lastmod [subst {
            select md5(sum(cal_item_id)::text || max(last_modified)::text)
            from   cal_items ci
            join   acs_objects ao on (ao.object_id = ci.cal_item_id)
            where  ci.on_which_calendar in
            ([join $clauses " union "])
        }]]
    }


    CalDAV instproc calcSyncToken {user_id} {
        # sync-token is the last modified value for the calendar items
        # needs to be in a format that can be converted back to the original value
        lappend clauses \
            {*}[calendars communityCalendarClause $user_id] \
            {*}[calendars alwaysQueriedClause $user_id]

        return [::xo::dc get_value lastmod [subst {
            select date_part('epoch', max(last_modified))::numeric::integer from cal_items ci
            join acs_objects ao on (ao.object_id = ci.cal_item_id) where ci.on_which_calendar in
            ([join $clauses " union "])
        }]]
    }

    CalDAV instproc parseRequest {content} {
        try {
            #dom setResultEncoding utf-8
            set document [dom parse -- $content]
        } on error {errorMsg} {
            ns_log error "CalDAV: parsing of request lead to error: $errorMsg!\n$content"
            throw {DOM PARSE {dom parse triggered exception}} $errorMsg
        }
        return $document
    }

    ###
    ###  HTTP method DELETE
    ###

    CalDAV instproc DELETE {} {
        set content [:getcontent {If-Match ""}]
        set url [ns_conn url]
        set user_id ${:user_id}
        # GN TODO: use ${:uri}, use in code ${:user_id}
        ns_log notice "DELETE TODO <${:uri}> vs. <$url>"

        set uid [:get_uid_from_href $url]
        set headers [ns_conn headers]
        #read if-match header which denotes the etag
        set if_match [ns_set get $headers If-Match]
        set calendar_id [::xo::dc get_value private_cal "
    select calendar_id from calendars where private_p = 't' and owner_id = :user_id limit 1" 0]

        :log "UID: $uid, calendar_id: $calendar_id: if_match $if_match"

        set item_exists [calendars get_cal_item_from_uid -calendar_ids $calendar_id $uid]
        ns_log notice "item_exists $item_exists"
        if {[llength $item_exists] > 0} {
            #todo: verify if deletion has finished
            foreach cal_item $item_exists {
                catch {::calendar::item::delete -cal_item_id $cal_item} err
                ns_log notice "calendar item delete: cal_item: $cal_item - err: $err"
            }
            set code 204
            set mimetype "text/plain"
            set response ""
        } else {
            set response [subst {
                <?xml version="1.0" encoding="utf-8" ?>
                <d:multistatus xmlns:d="DAV:">
                <d:response>
                <d:href>$url</d:href>
                <d:status>HTTP/1.1 404 Not Found</d:status>
                </d:response>
                </d:multistatus>}]
            set code 207
            set mimetype "text/xml"
        }
        :response $code $mimetype $response
    }

    ###
    ###  HTTP method PUT
    ###

    CalDAV ad_instproc PUT {} {

        UPDATE (SAVE?) a single calendar item denoted by a uid of a
        calendar item.

    } {
        set content [:getcontent {If-Match ""}]
        set ifmatch [ns_set iget [ns_conn headers] If-Match "*"]

        #
        # The request URI has to end with UID.ics, but we get the
        # actual UID from the ical UID field of the parsed content.
        #
        if {![string match *.ics ${:uri}]} {
            return [:request_error "Trying to PUT on a calendar that does not end with *.ics: ${:uri}"]
        }

        try {
            set sync_calendar_id [::caldav::get_sync_calendar -user_id ${:user_id}]
        } on error {errorMsg} {
            return [:request_error "no private calendar found for ${:user_id}"]
        }

        set items [calendars parse $content]
        :debug "caldav parser returned items <$items>"

        foreach item $items {
            set uid [$item cget -uid]
            :debug [$item serialize]

            if {$ifmatch eq "*"} {
                #
                # Create new entry
                #
                :debug "add a new entry"
                set calendar_id $sync_calendar_id
                #
                # We should probably check, whether we can really
                # write on the calendar in the general case, but here,
                # we know, the sync calendar is always writable for the
                # current user.
                #
                # - GN TODO: not sure, when mutating the UID is necessary, and
                #   whether adding a suffix with the user_id is the best solution.
                #   So we deactivate this code for now....
                #
                if {0} {
                    set user_id ${:user_id}
                    if {[::xo::dc get_value -prepare varchar,integer uid_exists {
                        select 1 from cal_uids u
                        join acs_objects o on (o.object_id = u.on_which_activity)
                        where cal_uid = :uid
                        and o.creation_user != :user_id
                    } 0]} {
                        ad_log warning "uid already exists for another user, suffixing ${uid}-${:user_id}"
                        $item set uid "${uid}-${:user_id}"
                    }
                }
            } else {
                #
                # Update an existing entry
                #
                :debug "update existing entry..."

                lappend clauses \
                    {*}[calendars communityCalendarClause ${:user_id}] \
                    {*}[calendars alwaysQueriedClause ${:user_id}]

                set all_calendar_ids [::xo::dc list read_only_cals [subst {
                    select calendar_id from ([join $clauses " union "]) as cals
                }]]

                set cal_infos [calendars get_calendar_and_cal_item_from_uid -calendar_ids $all_calendar_ids $uid]
                :debug "cal_infos for calendar_ids $all_calendar_ids uid $uid -> <$cal_infos>"

                if {$cal_infos ne ""} {
                    set cal_info [lindex $cal_infos 0]
                    lassign $cal_info calendar_id cal_item_id

                    :debug "write check needed? [expr {$calendar_id ne $sync_calendar_id}]"

                    if {$calendar_id != $sync_calendar_id} {
                        set can_write_p [permission::permission_p -object_id $cal_item_id -privilege write]
                        :debug "write check: $can_write_p (calendar_id $calendar_id, sync_calendar_id $sync_calendar_id)"

                        if {!$can_write_p} {
                            ns_log warning "CalDAV: user tried to perform a PUT on a read only item item: $cal_item_id user ${:user_id}"
                            if {[string match "*CalDavSynchronizer*" ${:user_agent}]} {
                                #
                                # Outlook synchronizer will continue
                                # hammering if 403 is returned,
                                # therefore try a different status
                                # code.
                                #
                                # GN TODO: please check, what happens
                                # with status code 412 on Outlook,
                                # since 202 probably silently swallows
                                # the update, which never happens.
                                #
                                :debug "CalDav: outlook client encountered"
                                set statusCode 202
                            } else {
                                set statusCode 412
                            }
                            return [:response $statusCode text/plain {}]
                        }
                    }
                }
            }
            #
            # save the item
            #
            :debug "updating item [$item cget -uid] in calendar $sync_calendar_id"
            :item_update -calendar_id $sync_calendar_id -item $item

            #ns_log notice "CalDAV PUT: [$item serialize]"
            ns_set put [ns_conn outputheaders] ETag [subst {"[:getETagByUID [$item cget -uid]]"}]
            $item destroy
            :response 201 text/plain {}
            return
        }
        # TODO what happens here
    }

    CalDAV instproc item_update {
        {-calendar_id:integer}
        {-item:object}
    } {
        #
        # This method inserts or updates a caldav calendar
        # item. If there is already a cal_item for this uid, we
        # perform an update on the original calendar. If it does
        # not exists, we perform an insert in the specified
        # calendar (calendar_id).
        #
        # The caller is responsible to acertain that the calendar
        # is writable.
        #
        # @param calendar_id place where new calendar items are added to

        set summary [$item get summary]
        if {$summary eq ""} {
            :log "CalDAV: summary is empty, skip this item"
            return
        }
        set uid [$item cget -uid]

        # TODO: is the following comment useful?
        #
        # We need to check if the item exists in one of the other
        # calendars of the user.  If an item with this uid already
        # exists, return an error

        set cal_item_id [lindex [::caldav::calendars get_cal_item_from_uid -calendar_ids $calendar_id $uid] 0]

        set dtend   [$item get dtend]
        set dtstart [$item get dtstart]
        #
        # TODO: as_ical_event checks as well for is_day_item
        #
        if { [clock scan $dtend] - [clock scan $dtstart] == 86400
             && [clock format [clock scan $dtstart] -format %H%M] eq "0000"
             && [clock format [clock scan $dtend]   -format %H%M] eq "0000"
         } {
            #ns_log notice "this is an all day event! ${:dtstart} ${:dtend}"
            #we set end to start as this is the way openacs calendar marks all day events
            set :dtend $dtstart
            $item is_day_item set true
        }
        set description [$item get description]
        set location    [$item get location]
        set ical_vars   [$item get ical_vars]

        if {$cal_item_id eq ""} {
            #
            # Create a new item
            #
            :debug "create a new item"
            set cal_item_id [calendar::item::new \
                                 -start_date ${dtstart} \
                                 -end_date ${dtend} \
                                 -name ${summary} \
                                 -description $description \
                                 -calendar_id $calendar_id \
                                 -location $location \
                                 -cal_uid $uid \
                                 -ical_vars $ical_vars]

            $item add_recurrence -cal_item_id $cal_item_id

        } else {
            #
            # Update an existing item
            #
            :debug "update/edit cal_item_id $cal_item_id uid <$uid> ical_vars $ical_vars"

            calendar::item::edit \
                -cal_item_id $cal_item_id \
                -start_date ${dtstart} \
                -end_date ${dtend} \
                -name ${summary} \
                -description ${description} \
                -location ${location} \
                -calendar_id $calendar_id \
                -edit_all_p 1 \
                -ical_vars ${ical_vars} \
                -cal_uid ${uid}

            set recurrence_id [::xo::dc get_value -prepare integer get_recurrence {
                select recurrence_id from acs_events where event_id = :cal_item_id
            }]

            $item edit_recurrence \
                -cal_item_id $cal_item_id \
                -recurrence_id $recurrence_id
        }
    }


    #CalDAV instproc handle_request {} {
    #    set ms [clock milliseconds]
    #    next
    #    if {[parameter::get_global_value -package_key caldav -parameter "debugmode"] > 0} {
    #        ns_log notice "CalDav ${:method} request took [expr {[clock milliseconds] - $ms}]ms"
    #    }
    #}

    CalDAV instproc getETagByUID {uid} {
        #note: last_modified is updated for the acs_event/cal_item object_id, not the acs_activity
        #when has this collection item been modified the last time?
        # TODO: do we need "max()"? 2 times
        set c_uid [::xo::dc get_value -prepare varchar select_last_modified_uid {
            select max(md5(last_modified::text))
            from cal_uids c, acs_objects ao, acs_events e
            where c.on_which_activity = e.activity_id
            and e.event_id = ao.object_id
            and cal_uid = :uid
        }]
        # fallback for events without an uid
        if {$c_uid eq "" && [string is integer -strict $uid]} {
            set c_uid [::xo::dc get_value -prepare integer select_last_modified_uid {
                select max(md5(last_modified::text))
                from  acs_objects ao, acs_events e
                where e.event_id = ao.object_id
                and e.activity_id = :uid
            }]
        }
        return $c_uid
    }
}

namespace eval ::caldav {
    CalDAV create ::caldav::dav

    ::caldav::dav eval {
        array set :xmlns {
            "DAV:"                          d
            "http://calendarserver.org/ns/" cs
            "urn:ietf:params:xml:ns:caldav" c
            "http://apple.com/ns/ical/"     ical
        }
        set :namespaces {}
        foreach {ns prefix} [array get :xmlns] {
            lappend :namespaces $prefix $ns
        }
    }
}

::xo::library source_dependent

#
# Local variables:
#    mode: tcl
#    tcl-indent-level: 4
#    indent-tabs-mode: nil
# End: