- Publicity: Public Only All
tDAV-procs.tcl
Procedures in this file
- tdav::check_lock (public)
- tdav::check_lock_for_unlock (public)
- tdav::conn (public)
- tdav::copy_props (public)
- tdav::dbm_read_list (private)
- tdav::dbm_write_list (private)
- tdav::delete_props (public)
- tdav::get_lock_file (public)
- tdav::get_user_props (public)
- tdav::lock_timeout_left (private)
- tdav::read_lock (private)
- tdav::remove_lock (public)
- tdav::respond (public)
- tdav::set_lock (public)
- tdav::update_user_props (public)
- tdav::write_lock (private)
Detailed information
tdav::check_lock (public)
tdav::check_lock uri
Check whether the request may proceed under the resource's WebDAV lock. Remove an expired lock. If an active lock has no matching token in the If header, send HTTP 423.
- Parameters:
- uri (required)
- Resource URI.
- Returns:
- unlocked if the request may proceed, or filter_return after HTTP 423.
- Partial Call Graph (max 5 caller/called nodes):
- Testcases:
- No testcase defined.
tdav::check_lock_for_unlock (public)
tdav::check_lock_for_unlock uri
Check the Lock-Token header against the stored lock for an UNLOCK request.
- Parameters:
- uri (required)
- Resource URI.
- Returns:
- unlocked if the token matches or no lock exists, or filter_return if the token is missing or does not match.
- Partial Call Graph (max 5 caller/called nodes):
- Testcases:
- No testcase defined.
tdav::conn (public)
tdav::conn [ args... ]
Read or write tDAV request state. A read falls back to ns_conn when no tDAV value is set for the requested field.
- Returns:
- The value read or stored.
- Partial Call Graph (max 5 caller/called nodes):
- Testcases:
- No testcase defined.
tdav::copy_props (public)
tdav::copy_props uri newuri
Copy persistent user properties from one resource to another.
- Parameters:
- uri (required)
- Source resource URI.
- newuri (required)
- Destination resource URI.
- Partial Call Graph (max 5 caller/called nodes):
- Testcases:
- No testcase defined.
tdav::dbm_read_list (private)
tdav::dbm_read_list uri
Read a resource's stored WebDAV user properties.
- Parameters:
- uri (required)
- Resource URI.
- Returns:
- A list of property key and value pairs, or an empty list if none are stored.
- Partial Call Graph (max 5 caller/called nodes):
- Testcases:
- No testcase defined.
tdav::dbm_write_list (private)
tdav::dbm_write_list uri list
Store a resource's WebDAV user properties as a persistent client property.
- Parameters:
- uri (required)
- Resource URI.
- list (required)
- Alternating property keys and values.
- Partial Call Graph (max 5 caller/called nodes):
- Testcases:
- No testcase defined.
tdav::delete_props (public)
tdav::delete_props uri
Clear the persistent user properties for a resource.
- Parameters:
- uri (required)
- Resource URI.
- Partial Call Graph (max 5 caller/called nodes):
- Testcases:
- No testcase defined.
tdav::get_lock_file (public)
tdav::get_lock_file uri
Return the path to the lock file for a resource, creating its parent directory if needed. Use the configured lockdir or the default lockdir next to the page root.
- Parameters:
- uri (required)
- Resource URI.
- Returns:
- Lock file path.
- Partial Call Graph (max 5 caller/called nodes):
- Testcases:
- No testcase defined.
tdav::get_user_props (public)
tdav::get_user_props uri depth prop_req
Read all stored user properties for a resource.
- Parameters:
- uri (required)
- Resource URI.
- depth (required)
- Requested traversal depth. Accepted but unused.
- prop_req (required)
- Requested property selection. Accepted but unused.
- Returns:
- A list of property key and value pairs.
- Partial Call Graph (max 5 caller/called nodes):
- Testcases:
- No testcase defined.
tdav::lock_timeout_left (private)
tdav::lock_timeout_left timeout locktime
Calculate the remaining lifetime of a WebDAV lock.
- Parameters:
- timeout (required)
- Lock lifetime in seconds.
- locktime (required)
- Lock creation time in a format accepted by clock scan.
- Returns:
- Remaining seconds, or zero if the lock has expired.
- Partial Call Graph (max 5 caller/called nodes):
- Testcases:
- No testcase defined.
tdav::read_lock (private)
tdav::read_lock uri
Read and decode a resource's lock record from its lock file. Create an empty lock file if it is absent.
- Parameters:
- uri (required)
- Resource URI.
- Returns:
- Lock record as a list, or an empty list for an empty file.
- Partial Call Graph (max 5 caller/called nodes):
- Testcases:
- No testcase defined.
tdav::remove_lock (public)
tdav::remove_lock uri
Delete the stored lock file for a resource.
- Parameters:
- uri (required)
- Resource URI.
- Partial Call Graph (max 5 caller/called nodes):
- Testcases:
- No testcase defined.
tdav::respond (public)
tdav::respond response
Send a WebDAV response. Format the response for the current HTTP method, with a plain text response for HTTP 423.
- Parameters:
- response (required)
- List starting with the HTTP status code, followed by method-specific response data.
- Partial Call Graph (max 5 caller/called nodes):
- Testcases:
- No testcase defined.
tdav::set_lock (public)
tdav::set_lock uri depth type scope owner [ timeout ] [ locktime ]
Create and store a WebDAV lock for a resource.
- Parameters:
- uri (required)
- Resource URI.
- depth (required)
- Lock depth.
- type (required)
- Lock type.
- scope (required)
- Lock scope.
- owner (required)
- Lock owner.
- timeout (optional)
- Lock lifetime. An empty value uses the configured default.
- locktime (optional)
- Lock creation time. An empty value uses the current time.
- Returns:
- The generated lock token.
- Partial Call Graph (max 5 caller/called nodes):
- Testcases:
- No testcase defined.
tdav::update_user_props (public)
tdav::update_user_props uri prop_req
Apply set and remove operations to a resource's stored user properties.
- Parameters:
- uri (required)
- Resource URI.
- prop_req (required)
- Alternating action and property records. Each property record contains a namespace/name key and a value.
- Returns:
- A list of HTTP status and property key pairs.
- Partial Call Graph (max 5 caller/called nodes):
- Testcases:
- No testcase defined.
tdav::write_lock (private)
tdav::write_lock uri list
Write a resource's lock record to its lock file, replacing any prior record.
- Parameters:
- uri (required)
- Resource URI.
- list (required)
- Lock record containing type, scope, owner, token, timeout, depth, and creation time.
- Partial Call Graph (max 5 caller/called nodes):
- Testcases:
- No testcase defined.
Content File Source
# # tDAV.tcl # # Copyright 2003 Musea Technologies # # http://www.museatech.net # # $Id # # bugs to: # toddg@tdav.museatech.net # # Authors: Todd Gillespie # Dave Bauer # # Based upon sources from: # # webdav.tcl # # A WebDAV implementation for AOLserver 3.x. # # Copyright (c) 2000-2001 Panoptic Computer Network. # All rights reserved. # # http://www.panoptic.com/ # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # ------------------------------------------------------------ # Silly workaround so that AOLserver can find scripts via "package require". # set tcl_library [file join $tcl_pkgPath tcl${tcl_version}] # source [file join $tcl_library init.tcl] # ------------------------------------------------------------ package require tdom namespace eval tdav {} # workaround if not installed in OACS # tdav::filter_webdav_options # # Handles OPTIONS HTTP requests # # Arguments: # none # # Results: # returns an HTTP response containing WebDAV options supported # # TODO Make this smart to return options based on URI # We still need to pretend that the site root supports DAV # methods or some clients get confused. proc tdav::filter_webdav_options {args} { set dav_level {1,2} ns_set put [ns_conn outputheaders] DAV $dav_level # The allowed webdav options for the share that the requested # URL belongs to. foreach {uri options} [nsv_array get tdav_options] { if {[regexp $uri [ns_conn url]]} { ns_set put [ns_conn outputheaders] Allow [join $options {, }] break } } # This tells MSFT products to skip looking for FrontPage extensions. ns_set put [ns_conn outputheaders] MS-Author-Via DAV ns_return 200 text/plain {} return filter_return } # ------------------------------------------------------------ # first check XML validity # add PROPPATCH # split into prop error function? # get body proc tdav::xml_valid_p {xml_doc} { # TODO use tnc with tDOM to validate the xml request return 1 } # tdav::read_xml # # reads xml from connection # # Arguments: # none # # Results: # # returns xml text of request proc tdav::read_xml {} { set xml [ns_getcontent -as_file false -binary false] ns_log debug "\n-----tdav::read_xml XML = -----\n $xml \n ----- end ----- \n " return $xml } # tdav::dbm_write_list # # helper fxns for dbm-like props # Writes a list to a properties file # # Arguments: # uri URI of the request being handled # list properties formatted in a Tcl list as # propertyname value # # Results: # file written including contents of list ad_proc -private ::tdav::dbm_write_list {uri list} { Store a resource's WebDAV user properties as a persistent client property. @param uri Resource URI. @param list Alternating property keys and values. } { regsub {^/} $uri {} uri ad_set_client_property \ -persistent t \ -session_id 0 \ oacs-dav $uri $list } # tdav::get_prop_file # # Get the filename that contains user properties. # # Arguments: # uri URI to get properties filename for # # Results: # Returns the filename containing user properties. proc tdav::get_prop_file {uri} { # just in case. I hate that 'file join' fails on this regsub {^/} $uri {} uri # log this for failed config section set name [ns_config "ns/server/[ns_info server]/tdav" propdir] if {$name eq ""} { set name [file join $::acs::pageroot "../propdir/${uri}"] } else { set name [file join $name $uri] } # catch uncreated parent dirs here: if {![file exists [file dirname $name]]} { # no parent dir, create it: file mkdir [file dirname $name] # safe for public consumption? } return "${name}.prop" } # tdav::get_lock_file # # Get the filename of the lock file # # Arguments: # uri URI to get the lock filename for # # Results: # Returns the filename containing the lock information for URI ad_proc -public ::tdav::get_lock_file {uri} { Return the path to the lock file for a resource, creating its parent directory if needed. Use the configured lockdir or the default lockdir next to the page root. @param uri Resource URI. @return Lock file path. } { # just in case. I hate that 'file join' fails on this regsub {^/} $uri {} uri # log this for failed config section set name [ns_config "ns/server/[ns_info server]/tdav" lockdir] if {$name eq ""} { set name [file join $::acs::pageroot "../lockdir/${uri}"] } else { set name [file join $name $uri] } if {![file exists [file dirname $name]]} { # no parent dir, create it: file mkdir [file dirname $name] # safe for public consumption? } return "${name}.lock" } # tdav::delete_props # # Delete the properties file for a URI # # Arguments: # uri URI of properties file to delete # # Results: # File containing user properties for URI is deleted ad_proc -public ::tdav::delete_props {uri} { Clear the persistent user properties for a resource. @param uri Resource URI. } { regsub {^/} $uri {} uri ad_set_client_property \ -persistent t \ -session_id 0 \ oacs-dav $uri "" } # tdav::move_props # # Move the properties file for a URI # # Arguments: # uri Original URI # newuri New URI after move # # Results: # Properties file is moved under the properties directory # to the relative location for newuri proc tdav::move_props {uri newuri} { regsub {^/} $uri {} uri regsub {^/} $newuri {} newuri set props [ad_get_client_property \ -session_id 0 \ -default "" \ oacs-dav $uri] ad_set_client_property \ -persistent t \ -session_id 0 \ oacs-dav $newuri $props ad_set_client_property \ -persistent t \ -session_id 0 \ oacs-dav $uri "" } # tdav::copy_props # # Copy properties file for a URI to another URI # # Arguments: # uri source URI to copy # newuri destination URI of copy # # Results: # Contents of properties file for URI is copied # under the properties directory to the relative # location corresponding to newuri. ad_proc -public ::tdav::copy_props {uri newuri} { Copy persistent user properties from one resource to another. @param uri Source resource URI. @param newuri Destination resource URI. } { regsub {^/} $uri {} uri regsub {^/} $newuri {} newuri set props [ad_get_client_property \ -session_id 0 \ -default "" \ oacs-dav $uri] ad_set_client_property \ -persistent t \ -session_id 0 \ oacs-dav $newuri $props } ad_proc -private ::tdav::write_lock {uri list} { Write a resource's lock record to its lock file, replacing any prior record. @param uri Resource URI. @param list Lock record containing type, scope, owner, token, timeout, depth, and creation time. } { set f [open [tdav::get_lock_file $uri] w] puts $f $list close $f } ad_proc -private ::tdav::dbm_read_list {uri} { Read a resource's stored WebDAV user properties. @param uri Resource URI. @return A list of property key and value pairs, or an empty list if none are stored. } { regsub {^/} $uri {} uri return [ad_get_client_property \ -session_id 0 \ -default "" \ oacs-dav $uri] } # tdav::read_lock # # Read lock file for a URI # # Arguments: # uri URI to retrieve lock # # Results: # Returns the contents of the lock file. Contents will # be evaluated before being returned. ad_proc -private ::tdav::read_lock {uri} { Read and decode a resource's lock record from its lock file. Create an empty lock file if it is absent. @param uri Resource URI. @return Lock record as a list, or an empty list for an empty file. } { set f [open [tdav::get_lock_file $uri] {CREAT RDONLY}] set s [read $f] set e "list ${s}" set l [eval $e] close $f return $l } # tdav::remove_lock # # Delete lock file, effectively also removing the lock # # Arguments: # uri URI to remove lock from # # Results: # Lock file for URI is deleted ad_proc -public ::tdav::remove_lock {uri} { Delete the stored lock file for a resource. @param uri Resource URI. } { file delete -- [tdav::get_lock_file $uri] } # tdav::dbm_write_array # # Write array into user properties file # # UNUSED proc tdav::dbm_write_array {uri arr} { # extract list from array tdav::dbm_write_list($uri,[array get arr]) # throw errors } # tdav::lock_timeout_left # # timeout # total length of timeout set in seconds # # locktime # time lock was created in any format clock scan can accept # ad_proc -private ::tdav::lock_timeout_left { timeout locktime } { Calculate the remaining lifetime of a WebDAV lock. @param timeout Lock lifetime in seconds. @param locktime Lock creation time in a format accepted by clock scan. @return Remaining seconds, or zero if the lock has expired. } { set locktime [clock scan $locktime] set lockexpiretime [clock scan "$timeout seconds" -base $locktime] set timeout_left [expr {$lockexpiretime - [clock seconds]}] if {$timeout_left < 0} { set timeout_left 0 } return $timeout_left } # tdav::check_lock # # Compare existing lock to lock token provided # by the client # # Arguments: # uri URI of request # # Results: # If the lock token in the Lock-Token header matches # an existing lock return "unlocked". Processing of # transaction from the caller should continue. If # the lock doesn't match return "filter_return". Generally # this means either no Lock-Token header was provided or # the Lock-Token header does not match the existing lock # on URI. In this case the caller should return an HTTP # status of 423 or otherwise treat the file as locked. ad_proc -public ::tdav::check_lock {uri} { Check whether the request may proceed under the resource's WebDAV lock. Remove an expired lock. If an active lock has no matching token in the If header, send HTTP 423. @param uri Resource URI. @return unlocked if the request may proceed, or filter_return after HTTP 423. } { regsub {^/} $uri {} uri # if lock exists, work. if not, just return. if {[file exists [tdav::get_lock_file $uri]]} { set lockinfo [tdav::read_lock $uri] # check if lock is expired if {[tdav::lock_timeout_left [lindex $lockinfo 4] [lindex $lockinfo 6]] == 0 } { tdav::remove_lock $uri return "unlocked" } set hdr [ns_set iget [ns_conn headers] If] # the If header exists, work, otherwise 423 if {[info exists hdr] && [string length $hdr]} { set token "" # add ? in the token re in case there is a conditional () # in the header regexp {(<https?://[^/]+([^>]+)>\s+)?\(<([^>]+)>\)} $hdr nil maybe hdr_uri token set ftk [lindex $lockinfo 3] if {![info exists token] || $token ne $ftk } { ns_log Debug "tdav::check_lock: token mismatch $ftk expected hdr: $hdr token: $token" ns_return 423 {text/plain} {} return filter_return } } else { ns_log Debug "tdav::check_lock: no \"If\" header found for request of $uri" ns_return 423 {text/plain} {} return filter_return } # also check for uri == hdr_uri } return unlocked } # tdav::check_lock_for_unlock # # Compare existing lock with client provided lock token. # # Arguments: # uri URI of the request # # Results: # Return "unlocked" for a matching token or no stored lock, # and "filter_return" for a missing or mismatched token. ad_proc -public ::tdav::check_lock_for_unlock {uri} { Check the Lock-Token header against the stored lock for an UNLOCK request. @param uri Resource URI. @return unlocked if the token matches or no lock exists, or filter_return if the token is missing or does not match. } { regsub {^/} $uri {} uri # if lock exists, work. if not, just return. if {[file exists [tdav::get_lock_file $uri]]} { set hdr [ns_set iget [ns_conn headers] {Lock-Token}] # the If header exists, work, otherwise 423 if {[info exists hdr] && [string length $hdr]} { regexp {<([^>]+)>} $hdr nil token set ftk [lindex [tdav::read_lock $uri] 3] if {[info exists token] && $token eq $ftk} { # it's good, the tokens match. carry on. } else { return filter_return } } else { return filter_return } # also check for uri == hdr_uri } return unlocked } # tdav::get_fs_props # # Generate a list of filesystem properties # # Arguments: # none # # Results: # Returns a list of standard DAV properties for # the request uri as ns_conn url # The list is formatted as # {namespace propertyname} value pairs. The results # should be evaluated in the caller. proc tdav::get_fs_props {} { # global fs_props set fs_props [list] # lappend fs_props [list ns0 supportlock] {subst {"<none/>"}} lappend fs_props [list ns0 getcontenttype] {subst {[ns_guesstype $filename]}} lappend fs_props [list D getcontentlength] {subst {[file size $entry]}} lappend fs_props [list D creationdate] {subst {[clock format $file_stat(mtime) -format "%Y-%m-%dT%H:%M:%SZ" -gmt 1]}} lappend fs_props [list D getlastmodified] {subst {[clock format $file_stat(mtime) -format "%a, %d %b %Y %H:%M:%S %Z" -gmt 1]}} lappend fs_props [list D getetag] {subst {"1f9a-400-3948d0f5"}} lappend fs_props [list D resourcetype] {if {[file isdirectory $entry]} { subst {D:collection} } else { subst {[ns_guesstype $filename]} }} return $fs_props } # tdav::extract_propertyupdate_remove # # I am guessing this should return a list of properties # to be removed. It isn't used anywhere. proc tdav::extract_propertyupdate_remove {proplist} { # ht # ACTION foreach c $proplist { # extraneous, then name set p [[$c childNodes] childNodes] set name [$p nodeName] # DATA: set ht($name) [[$p childNodes] nodeValue] } return $ht } # tdav::extract_propertyupdate_set # # I am guessing this should return a list of properties # to be removed. It isn't used anywhere. proc tdav::extract_propertyupdate_set {proplist} { # ht # ACTION foreach c $proplist { # extraneous, then name set p [[$c childNodes] childNodes] set name [$p nodeName] # DATA: set ht($name) [[$p childNodes] nodeValue] } return $ht } # tdav::filter_webdav_proppatch # # Prepare request data for PROPPATCH method # # Arguments: # none # # Results: # Parses XML body and puts the formatted result in tdav_conn(prop_req) # global variable. Accessed from tdav::conn prop_req command. # Sets tdav_conn(depth) from HTTP Depth header proc tdav::filter_webdav_proppatch {args} { set depth [tdav::conn -set depth [ns_set iget [ns_conn headers] Depth]] set xml [tdav::read_xml] if {[catch {dom parse -- $xml} xd]} { # xml body is not well formed ns_returnbadrequest return filter_return } set property_update [$xd documentElement] set prop_req [list] foreach node [$property_update childNodes] { regsub {^.*:} [$node nodeName] {} operation set p [[$node firstChild] firstChild] # we use localname because we always resolve the URI namespace # for the tag name set ns [$p namespaceURI] if {$ns eq ""} { set name [$p nodeName] } else { set name [$p localName] } if {[catch {set value [[$p childNodes] nodeValue]}]} { set value "" } lappend prop_req $operation [list [list $ns $name] $value] } tdav::conn -set prop_req $prop_req return filter_ok } # tdav::webdav_proppatch # # Handle proppatch method for tDAV filesystem storage # # Arguments: # none # # Results: # Attempts to set or unset properties based on the request # contained in tdav_conn(prop_req). # # Returns a list containing the HTTP status code and # the status of each property set/unset. The status is a list # of HTTP status code and text for each property. proc tdav::webdav_proppatch {} { set uri [ns_conn url] regsub {^/} $uri {} uri set filename [file join $::acs::pageroot $uri] set body "" set ret_code 200 if {![file exists $filename]} { set ret_code 404 } else { if {"unlocked" ne [tdav::check_lock $uri] } { set ret_code 423 set response "The resource is locked" } else { set prop_req [tdav::conn prop_req] set response [tdav::update_user_props $uri $prop_req] } set ret_code 207 } tdav::respond [list $ret_code $response] } # tdav::webdav_propfind # # Handle propfind request for tDAV filesystem storage # # Arguments: # none # # Results: # Returns a list of HTTP status for the request, and if successful a # list of properties in the format of # {href collection_p {properies_list}} # where properties list is a list of pairs # {namespace name} value. proc tdav::webdav_propfind {} { set props [list] set uri [ns_conn url] set depth [tdav::conn depth] set prop_req [tdav::conn prop_req] regsub {^/} $uri {} uri regsub -all -- (\{|\}) $uri \\\\& uri # decide on file or directory # why doesn't Tcl handle this? # otoh, it lets us handle the notfound error here # wait, no, this is right as long as the DAV request is correct # so fuck it if {$depth > 0} { set entries [glob -nocomplain [file join $::acs::pageroot $uri *]] } else { set entries [glob -nocomplain [file join $::acs::pageroot $uri]] } foreach entry $entries { set entry_props [list] set filename [lindex [file split $entry] end] # Tcl befuddles me: set href [string replace $entry 1 [string length $::acs::pageroot] ""] file stat $entry file_stat set collection_p [string equal "directory" $file_stat(type)] foreach {i j} [tdav::get_fs_props] { lappend entry_props [list [lindex $i 0] [lindex $i 1]] [eval $j] } foreach {i j} [tdav::get_user_props $uri $depth $prop_req] { lappend entry_props [list [lindex $i 0] [lindex $i 1]] $j } lappend props [list $href $collection_p $entry_props] } tdav::respond [list 207 $props] } # tdav::get_user_props # # Retrieve user properties from tDAV filesystem storage # # Arguments: # uri URI of the request # depth valid for collections (directories) can be 0 1 or infinity # 0 is the directory only # 1 is the directory and direct descendants # infinity is all descendants, this is the default if depth # is not specified # prop_req should contain a list of name/value pairs of properties # to return. Right now it is unsupported and all properties # are always returned # # Results: # returns a list of name/value pairs ad_proc -public ::tdav::get_user_props { uri depth prop_req } { Read all stored user properties for a resource. @param uri Resource URI. @param depth Requested traversal depth. Accepted but unused. @param prop_req Requested property selection. Accepted but unused. @return A list of property key and value pairs. } { regsub {^/} $uri {} luri return [tdav::dbm_read_list $luri] } ad_proc -public ::tdav::update_user_props {uri prop_req} { Apply set and remove operations to a resource's stored user properties. @param uri Resource URI. @param prop_req Alternating action and property records. Each property record contains a namespace/name key and a value. @return A list of HTTP status and property key pairs. } { array set props [tdav::dbm_read_list $uri] set status [list] foreach {action i} $prop_req { lassign $i k value switch -- $action { set { if {[catch {set props($k) $value} err]} { lappend status [list "HTTP/1.1 409 Conflict" $k] } else { lappend status [list "HTTP/1.1 200 OK" $k] } } remove { #according to WebDAV spec removing a nonexistent # property is not an error, if it's there # remove it, otherwise, continue. if {[info exists props($k)]} { unset props($k) } lappend status [list "HTTP/1.1 200 OK" $k] } } #filter out filesystem sets # DAVEB where is this filtering occurring? #write the props back out to disc: tdav::dbm_write_list $uri [array get props] } return $status } # tdav::filter_webdav_propfind # # Prepare incoming PROPFIND request # # Arguments: # none # # Results: # sets global values in tdav_conn array for # depth, and prop_req # prop_req is a list of lists of namespace/name pairs proc tdav::filter_webdav_propfind {args} { set prop_req [list] set depth [ns_set iget [ns_conn headers] Depth] tdav::conn -set depth $depth set body "" set ret_code 207 set xml [tdav::read_xml] # test for xml req # test for url existence regsub {^/} [ns_conn url] {} uri set entry [file join $::acs::pageroot $uri] # parse the xml body to check if its valid if {"" ne $xml && [catch {dom parse -- $xml} xd]} { ns_return 400 text/plain "XML request not well-formed." return filter_return } set xml_prop_list [list] if {[info exists xd] && "" ne $xd } { set prop [$xd getElementsByTagNameNS "DAV:" "prop"] # if <prop> element doesn't exist we return all properties if {$prop ne ""} { set xml_prop_list [$prop childNodes] } foreach node $xml_prop_list { set ns [$node namespaceURI] if {$ns eq ""} { set name [$node nodeName] } else { set name [$node localName] } lappend prop_req [list $ns $name] } } tdav::conn -set prop_req $prop_req # this should be the end of the filter. return filter_ok } # tdav::filter_webdav_put # # Prepare incoming PUT request # # Arguments: # none # # Results # Copies content to a temporary file and sets tdav_conn(tmpfile) proc tdav::filter_webdav_put {args} { tdav::conn -set tmpfile [ns_getcontent -as_file 1 -binary 1] return filter_ok } # tdav::webdav_put # # Handle PUT for tDAV filesystem storage # # Arguments: # none # # Results: # If successful file is created under AOLserver pageroot # that corresponds to the URI of the request. # Calls tdav::respond with a list containing HTTP status # and response body to return the results to the client. proc tdav::webdav_put {} { set uri [ns_conn url] set uri [string trimleft $uri "/"] set entry [file join $::acs::pageroot $uri] set filename [lindex [file split $entry] end] set tmpfile [tdav::conn tmpfile] set ret_code 500 set body "" if {[file exists $entry]} { if {"unlocked" ne [tdav::check_lock $uri] } { set ret_code 423 set body "Resource is locked." } else { file rename -force -- $tmpfile $entry set ret_code 204 } } else { file rename -- $tmpfile $entry set ret_code 201 } tdav::respond [list $ret_code ""] } # tdav::filter_webdav_delete # # Prepare incoming DELETE request # # Arguments: # none # # Results: # There isn't anything to set so this doesn't do anything # right now proc tdav::filter_webdav_delete {args} { # not sure there is anything we need to set here return filter_ok } # tdav::webdav_delete # # Handle DELETE method for tDAV filesystem storage # # Arguments: # none # # Results: # If successful file corresponding to URI is removed from # the filesystem. In addition properties and lock files # are also removed. Calls tdav::respond to return the results # to the client. proc tdav::webdav_delete {} { set uri [ns_conn url] regsub {^/} $uri {} uri set entry [file join $::acs::pageroot $uri] set filename [lindex [file split $entry] end] set ret_code 500 set body "" if {[file exists $entry]} { # 423's and returns: if {"unlocked" eq [tdav::check_lock $uri]} { file delete -force -- $entry tdav::delete_props $uri tdav::remove_lock $uri set ret_code 204 } else { set ret_code 423 set body "Resource is locked." } } else { # file exists will fail on urls created by urlencode. do a decode here & test # ? set ret_code 404 } tdav::respond [list $ret_code $body] } # tdav::filter_webdav_mkcol # # Prepares MKCOL request # # Arguments: # none # # Results: # This handles the invalid request with # a content body. Otherwise it passes on to the # registered procedure. proc tdav::filter_webdav_mkcol {args} { if {[ns_conn contentlength]} { set ret_code 415 set html_response "" tdav::respond [list 415] return filter_return } return filter_ok } # tdav::webdav_mkcol # # Handles MKCOL method for tDAV filesystem storage # # Arguments: # none # # Results: # Creates a directory under the AOLserver pageroot # corresponding to the URI. Calls tdav::respond to # return the results to the client. proc tdav::webdav_mkcol {} { set uri [ns_conn url] regsub {^/} $uri {} uri set entry [file join $::acs::pageroot $uri] set filename [lindex [file split $entry] end] regsub {/[^/]*/*$} $entry {} parent_dir if {![file exists $parent_dir]} { set ret_code 409 } elseif {![file exists $entry]} { file mkdir $entry file mkdir [file join $::acs::pageroot "../props/" $uri] set ret_code 201 } else { set ret_code 405 } tdav::respond [list $ret_code] } # ------------------------------------------------------------ proc tdav::filter_webdav_copy {args} { set overwrite [tdav::conn -set overwrite [ns_set iget [ns_conn headers] Overwrite]] set destination [encoding convertto utf-8 [ns_urldecode [ns_set iget [ns_conn headers] Destination]]] regsub {https?://[^/]+/} $destination {/} dest tdav::conn -set destination $dest return filter_ok } proc tdav::webdav_copy {} { set overwrite [tdav::conn overwrite] set dest [tdav::conn destination] set local_dest $::acs::pageroot append local_dest $dest set newuri [string replace $local_dest 1 [string length $::acs::pageroot] ""] regsub {^/} $newuri {} newuri set uri [ns_conn url] regsub {^/} $uri {} uri set entry [file join $::acs::pageroot $uri] set filename [lindex [file split $entry] end] regsub {^/} [ns_conn url] {} uri set entry [file join $::acs::pageroot $uri] if {![file exists $entry]} { set ret_code 404 } else { if {[file exists $local_dest]} { if {"unlocked" ne [tdav::check_lock $dest] } { # ns_return 423 {text/plain} {Resource is locked.} set ret_code 423 set body "Resource is locked." } else { if {[string equal -nocase $overwrite "F"]} { set ret_code 412 } else { set ret_code 204 file copy -force -- $entry $local_dest tdav::copy_props $uri $newuri } } } else { set ret_code 201 file copy -- $entry $local_dest tdav::copy_props $uri $newuri } } ns_return $ret_code {text/html} {} tdav::respond [list $ret_code] } # ------------------------------------------------------------ proc tdav::filter_webdav_move {args} { set overwrite [tdav::conn -set overwrite [ns_set iget [ns_conn headers] Overwrite]] set destination [encoding convertto utf-8 [ns_urldecode [ns_set iget [ns_conn headers] Destination]]] regsub {https?://[^/]+/} $destination {/} dest tdav::conn -set destination $dest return filter_ok } proc tdav::webdav_move { args } { set overwrite [tdav::conn overwrite] set dest [tdav::conn destination] set uri [ns_conn url] set local_dest $::acs::pageroot append local_dest $dest set newuri [string replace $local_dest 1 [string length $::acs::pageroot] ""] regsub {^/} $newuri {} newuri set uri [ns_conn url] regsub {^/} $uri {} uri set entry [file join $::acs::pageroot $uri] set filename [lindex [file split $entry] end] set ret_code 500 set body {} if {![file exists $entry]} { set ret_code 404 } else { if {"unlocked" ne [tdav::check_lock $uri] } { # ns_return 423 {text/plain} {Resource is locked.} set ret_code 423 set body "Resource is locked." } elseif {[file exists $local_dest]} { if {[string equal -nocase $overwrite "F"]} { set ret_code 412 } else { set ret_code 204 file delete -force -- $local_dest file copy -force -- $entry $local_dest file delete -force -- $entry tdav::copy_props $uri $newuri tdav::delete_props $uri } } else { set ret_code 201 file copy -- $entry $local_dest tdav::copy_props $uri $newuri file delete -force -- $entry tdav::delete_props $uri } } ns_return $ret_code {text/html} $body return filter_return } proc tdav::filter_webdav_lock {args} { set ret_code 500 set body {} set xml [tdav::read_xml] set d [[dom parse -- $xml] documentElement] set l [$d childNodes] set scope [[[lindex $l 0] childNodes] nodeName] set type [[[lindex $l 1] childNodes] nodeName] if {[catch {set owner [[[lindex $l 2] childNodes] nodeValue]} err]} { set owner "" } set depth [ns_set iget [ns_conn headers] Depth] set timeout [ns_set iget [ns_conn headers] Timeout] regsub {^Second-} $timeout {} timeout tdav::conn -set lock_timeout $timeout if {$depth eq ""} { set depth 0 } tdav::conn -set depth $depth tdav::conn -set lock_scope $scope tdav::conn -set lock_type $type tdav::conn -set lock_owner $owner set lock_token [ns_set iget [ns_conn headers] Lock-Token] tdav::conn -set lock_token $lock_token return filter_ok } ad_proc -public ::tdav::set_lock {uri depth type scope owner {timeout ""} {locktime ""} } { Create and store a WebDAV lock for a resource. @param uri Resource URI. @param depth Lock depth. @param type Lock type. @param scope Lock scope. @param owner Lock owner. @param timeout Lock lifetime. An empty value uses the configured default. @param locktime Lock creation time. An empty value uses the current time. @return The generated lock token. } { if {$timeout eq ""} { set timeout [ns_config "ns/server/[ns_info server]/tdav" "defaultlocktimeout" "300"] } if {$locktime eq ""} { set locktime [clock format [clock seconds] -format "%T %D"] } set token "opaquelocktoken:[ns_rand 2147483647]" set lock [list $type $scope $owner $token $timeout $depth $locktime] tdav::write_lock $uri $lock return $token } proc tdav::webdav_lock {} { set scope [tdav::conn lock_scope] set type [tdav::conn lock_type] set owner [tdav::conn lock_owner] set uri [ns_conn url] regsub {^/} $uri {} uri set entry [file join $::acs::pageroot $uri] set filename [lindex [file split $entry] end] set existing_lock_token [tdav::conn lock_token] # if {![file exists $entry]} { # set ret_code 404 # } else if {"unlocked" ne [tdav::check_lock $uri] } { set ret_code 423 tdav::respond [list $ret_code] } else { set depth [tdav::conn depth] set timeout [tdav::conn lock_timeout] if {$timeout eq ""} { #probably make this a parameter? set timeout 180 } if {"" ne $existing_lock_token && [file exists [tdav::get_lock_file $uri]} { set old_lock [tdav::read_lock $uri] set new_lock [list [lindex $old_lock 0] [lindex $old_lock 1] [lindex $old_lock 2] [lindex $old_lock 3] $timeout [clock format [clock seconds]]] tdav::write_lock $uri $new_lock } else { set token [tdav::set_lock $uri $depth $type $scope $owner $timeout [clock format [clock seconds]]] } set ret_code 200 tdav::respond [list $ret_code [list depth $depth token $token timeout $timeout owner $owner scope $scope type $type]] } } proc tdav::filter_webdav_unlock {args} { set ret_code 500 set body {} set lock_token [ns_set iget [ns_conn headers] Lock-Token] tdav::conn -set lock_token $lock_token return filter_ok } proc tdav::webdav_unlock {} { set uri [ns_conn url] regsub {^/} $uri {} uri set entry [file join $::acs::pageroot $uri] set filename [lindex [file split $entry] end] if {![file exists $entry]} { set ret_code 404 set body {} } elseif {"unlocked" ne [tdav::check_lock_for_unlock $uri] } { set ret_code 423 set body "Resource is locked." } else { tdav::remove_lock $uri set ret_code 204 set body "" } tdav::respond [list $ret_code $body] } proc tdav::filter_stuff_nsperm {args} { # should be something like "Basic 29234k3j49a" set authorization [ns_set iget [ns_conn headers] Authorization] if {$authorization ne ""} { set user [dict get $credentials user] # # GN: this is unfinished (but now fixed) code.... # } return filter_ok } proc tdav::return_unauthorized { {realm ""} } { ns_set put [ns_conn outputheaders] "WWW-Authenticate" [subst {Basic realm="$realm"}] ns_return 401 {text/plain} "Unauthorized\n" } # so this will take what's returned and if necessary format an # XML response body ad_proc -public ::tdav::respond { response } { Send a WebDAV response. Format the response for the current HTTP method, with a plain text response for HTTP 423. @param response List starting with the HTTP status code, followed by method-specific response data. } { set response_code [lindex $response 0] if {"423" eq $response_code} { set response_body "The resource is locked" set mime_type "text/plain" } else { set response_list [tdav::respond::[string tolower [ns_conn method]] $response] lassign $response_list response_body mime_type if {$mime_type eq ""} { set mime_type "text/plain" } if {[string match "text/xml*" $mime_type]} { set response_body [encoding convertto utf-8 $response_body] } } ns_log debug "\n ----- tdav litmus headers ----- \n [ns_set iget [ns_conn headers] X-Litmus] \n -----\n" ns_log debug "\n ----- tdav::response response_body ----- \n $response_body \n ----- end ----- \n" ns_return $response_code $mime_type $response_body } namespace eval tdav::respond {} proc tdav::respond::delete { response } { set body "" set mime_type text/plain set body [lindex $response 1] return [list $body $mime_type] } proc tdav::respond::lock { response } { array set lock [lindex $response 1] set body [subst {<?xml version="1.0" encoding="utf-8"?> <prop xmlns="DAV:"> <lockdiscovery> <activelock> <locktype><${lock(type)}/></locktype> <lockscope><${lock(scope)}/></lockscope> <depth>${lock(depth)}</depth> <owner>${lock(owner)}</owner><timeout>Second-${lock(timeout)}</timeout> <locktoken> <href>${lock(token)}</href> </locktoken> </activelock> </lockdiscovery> </prop>}] ns_set put [ns_conn outputheaders] "Lock-Token" "<${lock(token)}>" set ret_code 200 return [list $body text/html] } proc tdav::respond::unlock { response } { # probably should be doing something here set body "" return [list $body] } proc tdav::respond::put { response } { return $response } proc tdav::respond::proppatch { response } { set resp_code [lindex $response 0] set href "" set body [subst {<?xml version="1.0" encoding="utf-8" ?> <D:multistatus xmlns:D="DAV:"> <D:response xmlns:ns0="DAV:"> <D:href>[ns_conn location]${href}</D:href> }] foreach res [lindex $response 1] { set status [lindex $res 0] set ns [lindex $res 1 0] set name [lindex $res 1 1] append body [subst {<D:propstat> <D:prop><$name xmlns='$ns'/></D:prop> <D:status>$status</D:status> </D:propstat> }] } append body {</D:response> </D:multistatus>} return [list $body {text/xml charset="utf-8"}] } proc tdav::respond::copy { response } { return $response } proc tdav::respond::move { response } { return $response } proc tdav::respond::mkcol { response } { set body "" switch -- [lindex $response 0] { 415 { # set body "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">" } 490 { # set body "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">" } 201 { # set body "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\"> # <html><head> # <title>201 Created</title> # </head><body> # <h1>Created</h1> # <p>Collection [ns_conn url] has been created.</p> # <hr> # <address></address> # </body></html>" } 405 { set body "<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\"> <html><head> <title>405 Method Not Allowed</title> </head><body> <h1>Method not allowed</h1> </body></html>" } } return [list $body text/html] } proc tdav::respond::propfind { response } { # this proc requires that all properties to be returned are in the # response lindex 1 # we don't have to check the tdav fs props or lock properties # they should already be there set request_properties [list] foreach pr [tdav::conn prop_req] { lappend request_properties $pr "" } set d [dom createDocumentNS "DAV:" "D:multistatus"] set n [$d documentElement] $n setAttribute "xmlns:b" "urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/" set mst_body "" foreach res [lindex $response 1] { lassign $res href . props set r [$d createElementNS DAV: ns0:response] $n appendChild $r set h [$d createElement D:href] $h appendChild [$d createTextNode ${href}] $r appendChild $h # # We will return both the properties we have and the # properties that were requested (as per RFC). When the # requested property does not exist, 404 will be returned in # the status. # foreach {i j} [dict merge $request_properties $props] { set propstat [$d createElement D:propstat] set prop [$d createElement D:prop] $r appendChild $propstat # interestingly enough, adding the namespace here to the prop is fine lassign $i ns name if {"D" ne $ns && "ns0" ne $ns } { # for user properties set the namespace explicitly in # the tag if {$ns ne ""} { set pnode [$d createElementNS $ns $name] } else { set pnode [$d createElement $name] } } else { set pnode [$d createElement ${ns}:${name}] } if {"creationdate" eq $name} { $pnode setAttribute "b:dt" "dateTime.tz" } if {"getlastmodified" eq $name} { $pnode setAttribute "b:dt" "dateTime.rfc1123" } if {"D:collection" eq $j} { $pnode appendChild [$d createElement $j] } else { $pnode appendChild [$d createTextNode $j] } $prop appendChild $pnode set supportedlock [$d createElement D:supportedlock] set lockentry [$d createElement D:lockentry] set lockscope [$d createElement D:lockscope] set exclusive [$d createElement D:exclusive] set locktype [$d createElement D:locktype] set write_type [$d createElement D:write] $supportedlock appendChild $lockentry $locktype appendChild $write_type $lockscope appendChild $exclusive $lockentry appendChild $lockscope $lockentry appendChild $locktype $prop appendChild $supportedlock set lockdiscovery [$d createElement D:lockdiscovery] regsub {https?://[^/]+/} $href {/} local_uri if {[file exists [tdav::get_lock_file $local_uri]]} { # check for timeout set lockinfo [tdav::read_lock $local_uri] set lock_timeout_left [tdav::lock_timeout_left [lindex $lockinfo 4] [lindex $lockinfo 6]] if {$lock_timeout_left > 0} { set activelock [$d createElement D:activelock] set locktype [$d createElement D:locktype] set lockscope [$d createElement D:lockscope] set depth [$d createElement D:depth] set owner [$d createElement D:owner] set timeout [$d createElement D:timeout] set locktoken [$d createElement D:locktoken] set locktokenhref [$d createElement D:href] $locktype appendChild [$d createElement D:[lindex $lockinfo 0]] $lockscope appendChild [$d createElement D:[lindex $lockinfo 1]] $depth appendChild [$d createTextNode [lindex $lockinfo 5]] $timeout appendChild [$d createTextNode Second-$lock_timeout_left] $owner appendChild [$d createTextNode [lindex $lockinfo 2]] $locktokenhref appendChild [$d createTextNode [lindex $lockinfo 3]] $locktoken appendChild $locktokenhref $activelock appendChild $locktype $activelock appendChild $lockscope $activelock appendChild $depth $activelock appendChild $timeout $activelock appendChild $owner $activelock appendChild $locktoken $lockdiscovery appendChild $activelock } } $prop appendChild $lockdiscovery $propstat appendChild $prop set status [$d createElement D:status] set status_code [expr { [dict exists $props $i] ? "HTTP/1.1 200 OK" : "HTTP/1.1 404 Not Found" }] set status_text [$d createTextNode $status_code] $status appendChild $status_text $propstat appendChild $status } } set body [$d asXML -escapeNonASCII] set body "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n${body}" set response [list $body {text/xml charset="utf-8"}] return $response } ad_proc -public ::tdav::conn {args} { Read or write tDAV request state. A read falls back to ns_conn when no tDAV value is set for the requested field. @param args A field name, -get field, or -set field value. @return The value read or stored. } { global tdav_conn set flag [lindex $args 0] if { [string index $flag 0] ne "-" } { set var $flag set flag "-get" } else { set var [lindex $args 1] } switch -- $flag { -set { set value [lindex $args 2] set tdav_conn($var) $value return $value } -get { if { [info exists tdav_conn($var)] } { return $tdav_conn($var) } else { return [ns_conn $var] } } } } proc tdav::apply_filters {{uri "/*"} {options "OPTIONS GET HEAD POST DELETE TRACE PROPFIND PROPPATCH COPY MOVE MKCOL LOCK UNLOCK"} {enable_filesystem "f"}} { # Verify that the options are valid options. Webdav requires # support for a minimum set of options. And offers support for a # limited set of options. (See RFC 2518) set required_options [list OPTIONS PROPFIND PROPPATCH MKCOL GET HEAD POST] foreach required_option $required_options { if {$required_option ni [string toupper $options]} { ns_log error "Required option $required_option missing from tDAV options for URI '$uri'. Required web dav options are: '$required_options'." return } } set allowed_options [list OPTIONS COPY DELETE GET HEAD MKCOL MOVE LOCK POST PROPFIND PROPPATCH PUT TRACE UNLOCK] foreach option $options { if {[lsearch -exact $allowed_options [string toupper $option]] < 0} { ns_log error "Option $option is not an allowed tDAV option for URI '$uri'. Allowed web dav options are: '$allowed_options'." return } } # Register filters for selected tDAV options. Do not register a # filter for GET, POST or HEAD. # change /example/* to /example* to accommodate the # url matching for registered filters set filter_uri "[string trimright $uri /*]*" foreach option $options { if {$option ni [list GET POST HEAD]} { ns_log debug "tDAV registering filter for $filter_uri on $option" ns_register_filter postauth [string toupper $option] "${filter_uri}" tdav::filter_webdav_[string tolower $option] } } ns_log notice "tDAV: Registered filters on $filter_uri" # Register procedures for selected tDAV options. Do not register a # proc for OPTIONS, GET, POST or HEAD. if {"true" eq $enable_filesystem} { foreach option $options { if {$option ni [list OPTIONS GET POST HEAD]} { ns_log debug "tDAV registering proc for $uri on $option" ns_register_proc [string toupper $option] "${uri}" tdav::webdav_[string tolower $option] } } ns_log notice "tDAV: Registered procedures on $uri" } else { ns_log notice "tDAV: Filesystem access by WebDAV disabled" } # Store the tDAV properties in an nsv set so that the registered # filters and procedures don't have to read the config file # anymore. nsv_set tdav_options $uri $options } proc tdav::add_user {user encpass} { ns_perm adduser $user $encpass "" } proc tdav::setpass {user encpass} { ns_perm setpass $user $encpass } proc tdav::remove_user {user} { # no corresponding ns_perm function. # ns_perm setpass # ns_perm denyuser /* # might work } proc tdav::allow_user {uri user} { foreach {share_uri options} [nsv_array get tdav_options] { if {[regexp $share_uri $uri]} { foreach option $options { ns_perm allowuser [string toupper $option] ${uri} $user } break } } } proc tdav::deny_user {uri user} { foreach {share_uri options} [nsv_array get tdav_options] { if {[regexp $share_uri $uri]} { foreach option $options { ns_perm denyuser [string toupper $option] ${uri} $user } break } } } proc tdav::allow_group {uri group} { foreach {share_uri options} [nsv_array get tdav_options] { if {[regexp $share_uri $uri]} { foreach option $options { ns_perm allowgroup [string toupper $option] ${uri} $group } break } } } proc tdav::deny_group {uri group} { foreach {share_uri options} [nsv_array get tdav_options] { if {[regexp $share_uri $uri]} { foreach option $options { ns_perm denygroup [string toupper $option] ${uri} $group } break } } } # and finally, install all that. if {![nsv_exists tdav_filters_installed filters_installed]} { nsv_set tdav_filters_installed filters_installed 1 # Uncomment the default user and password for testing. The # application of permissions will be application specific. To use # ns_perm your application will need to fill the ns_perm data # every time the server is loaded and when anything changes in a # running server. SkipLocks must be set to On in the AOLserver # config file and ns_perm module must be loaded. # The alternative is to define preauth filters on the WebDAV # methods and write your own code to handle authentication. This # is how the OpenACS implementation that uses tDAV works. # ns_perm adduser tdav [ns_crypt tdav salt] userfield # ns_perm adduser tdav1 [ns_crypt tdav1 salt] userfield # ns_perm addgroup tdav tdav tdav1 set tdav_shares [ns_configsection "ns/server/[ns_info server]/tdav/shares"] if { "" ne $tdav_shares } { foreach s [ns_set keys $tdav_shares] { set tdav_share [ns_configsection "ns/server/[ns_info server]/tdav/share/$s"] tdav::apply_filters \ [ns_set iget $tdav_share uri] \ [ns_set iget $tdav_share options] \ [ns_set iget $tdav_share enablefilesystem] # uncomment the next line if you are using ns_perm authentication # tdav::allow_group [ns_set iget $tdav_share uri] tdav } } } # Local variables: # mode: tcl # tcl-indent-level: 4 # indent-tabs-mode: nil # End: