tDAV-procs.tcl
Does not contain a contract.
- Location:
- /packages/oacs-dav/tcl/tDAV-procs.tcl
Related Files
[ hide source ] | [ make this the default ]
File Contents
# # 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 proc tdav::dbm_write_list {uri list} { 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 proc tdav::get_lock_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" 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 proc tdav::delete_props {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. proc tdav::copy_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 } proc tdav::write_lock {uri list} { set f [open [tdav::get_lock_file $uri] w] puts $f $list close $f } proc tdav::dbm_read_list {uri} { 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. proc tdav::read_lock {uri} { 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 proc tdav::remove_lock {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 # proc tdav::lock_timeout_left { timeout locktime } { 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. proc tdav::check_lock {uri} { 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: # If the client provided lock token matches the existing lock the # lock is removed and "unlocked" is returned. Otherwise no action # is taken on the lock and "filter_return" is returned. proc tdav::check_lock_for_unlock {uri} { 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 proc tdav::get_user_props { uri depth prop_req } { regsub {^/} $uri {} luri return [tdav::dbm_read_list $luri] } proc tdav::update_user_props {uri prop_req} { 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} { set fd [ad_opentmpfile tmpfile] ns_writecontent $fd close $fd tdav::conn -set tmpfile $tmpfile 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 } proc tdav::set_lock {uri depth type scope owner {timeout ""} {locktime ""} } { 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 proc tdav::respond { response } { 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 } proc tdav::conn {args} { 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 get $tdav_share uri] [ns_set get $tdav_share options] [ns_set get $tdav_share enablefilesystem] # uncomment the next line if you are using ns_perm authentication # tdav::allow_group [ns_set get $tdav_share uri] tdav } } } # Local variables: # mode: tcl # tcl-indent-level: 4 # indent-tabs-mode: nil # End: