| blib/lib/Net/DAV/Server.pm | |||
|---|---|---|---|
| Criterion | Covered | Total | % | 
| statement | 37 | 39 | 94.8 | 
| branch | n/a | ||
| condition | n/a | ||
| subroutine | 13 | 13 | 100.0 | 
| pod | n/a | ||
| total | 50 | 52 | 96.1 | 
| line | stmt | bran | cond | sub | pod | time | code | 
|---|---|---|---|---|---|---|---|
| 1 | package Net::DAV::Server; | ||||||
| 2 | 12 | 12 | 664533 | use strict; | |||
| 12 | 32 | ||||||
| 12 | 427 | ||||||
| 3 | 12 | 12 | 65 | use warnings; | |||
| 12 | 21 | ||||||
| 12 | 325 | ||||||
| 4 | 12 | 12 | 17579 | use File::Slurp; | |||
| 12 | 229450 | ||||||
| 12 | 1018 | ||||||
| 5 | 12 | 12 | 37170 | use Encode; | |||
| 12 | 201145 | ||||||
| 12 | 1273 | ||||||
| 6 | 12 | 12 | 13052 | use File::Find::Rule::Filesys::Virtual; | |||
| 12 | 238206 | ||||||
| 12 | 146 | ||||||
| 7 | 12 | 12 | 14603 | use HTTP::Date qw(time2str time2isoz); | |||
| 12 | 61374 | ||||||
| 12 | 916 | ||||||
| 8 | 12 | 12 | 5868 | use HTTP::Headers; | |||
| 12 | 65745 | ||||||
| 12 | 410 | ||||||
| 9 | 12 | 12 | 12997 | use HTTP::Response; | |||
| 12 | 161428 | ||||||
| 12 | 387 | ||||||
| 10 | 12 | 12 | 5504 | use HTTP::Request; | |||
| 12 | 4932 | ||||||
| 12 | 358 | ||||||
| 11 | 12 | 12 | 200 | use File::Spec; | |||
| 12 | 25 | ||||||
| 12 | 284 | ||||||
| 12 | 12 | 12 | 57 | use URI; | |||
| 12 | 23 | ||||||
| 12 | 429 | ||||||
| 13 | 12 | 12 | 61 | use URI::Escape; | |||
| 12 | 19 | ||||||
| 12 | 814 | ||||||
| 14 | 12 | 12 | 1704959 | use XML::LibXML; | |||
| 0 | |||||||
| 0 | |||||||
| 15 | use XML::LibXML::XPathContext; | ||||||
| 16 | use Net::DAV::LockManager (); | ||||||
| 17 | use Net::DAV::LockManager::DB (); | ||||||
| 18 | |||||||
| 19 | our $VERSION = '1.305'; | ||||||
| 20 | $VERSION = eval $VERSION; # convert development version into a simpler version number. | ||||||
| 21 | |||||||
| 22 | our %implemented = ( | ||||||
| 23 | options => 1, | ||||||
| 24 | put => 1, | ||||||
| 25 | get => 1, | ||||||
| 26 | head => 1, | ||||||
| 27 | post => 1, | ||||||
| 28 | delete => 1, | ||||||
| 29 | mkcol => 1, | ||||||
| 30 | propfind => 1, | ||||||
| 31 | copy => 1, | ||||||
| 32 | lock => 1, | ||||||
| 33 | unlock => 1, | ||||||
| 34 | move => 1 | ||||||
| 35 | ); | ||||||
| 36 | |||||||
| 37 | sub new { | ||||||
| 38 | my $class = shift; | ||||||
| 39 | my %args = @_ % 2 ? () : @_; | ||||||
| 40 | my $self = {}; | ||||||
| 41 | if ( $args{'-dbobj'} ) { | ||||||
| 42 | $self->{'lock_manager'} = Net::DAV::LockManager->new( $args{'-dbobj'} ); | ||||||
| 43 | } | ||||||
| 44 | elsif ( $args{'-dbfile'} ) { | ||||||
| 45 | $self->{'_dsn'} = "dbi:SQLite:dbname=$args{'-dbfile'}"; | ||||||
| 46 | } | ||||||
| 47 | elsif ( $args{'-dsn'} ) { | ||||||
| 48 | $self->{'_dsn'} = $args{'-dsn'}; | ||||||
| 49 | } | ||||||
| 50 | bless $self, $class; | ||||||
| 51 | if ( $args{'-filesys'} ) { | ||||||
| 52 | $self->filesys( $args{'-filesys'} ); | ||||||
| 53 | } | ||||||
| 54 | return $self; | ||||||
| 55 | } | ||||||
| 56 | |||||||
| 57 | sub filesys { | ||||||
| 58 | my ($self, $nfs) = @_; | ||||||
| 59 | $self->{'-filesys'} = $nfs if defined $nfs; | ||||||
| 60 | return $self->{'-filesys'}; | ||||||
| 61 | } | ||||||
| 62 | |||||||
| 63 | sub run { | ||||||
| 64 | my ( $self, $request, $response ) = @_; | ||||||
| 65 | |||||||
| 66 | my $fs = $self->filesys || die 'Filesys missing'; | ||||||
| 67 | |||||||
| 68 | my $method = $request->method; | ||||||
| 69 | my $path = uri_unescape $request->uri->path; | ||||||
| 70 | |||||||
| 71 | if ( !defined $response ) { | ||||||
| 72 | $response = HTTP::Response->new; | ||||||
| 73 | } | ||||||
| 74 | |||||||
| 75 | $method = lc $method; | ||||||
| 76 | if ( $implemented{$method} ) { | ||||||
| 77 | $response->code(200); | ||||||
| 78 | $response->message('OK'); | ||||||
| 79 | eval { | ||||||
| 80 | $response = $self->$method( $request, $response ); | ||||||
| 81 | $response->header( 'Content-Length' => length( $response->content ) ) if defined $response->content; | ||||||
| 82 | 1; | ||||||
| 83 | } or do { | ||||||
| 84 | return HTTP::Response->new( 400, 'Bad Request' ); | ||||||
| 85 | }; | ||||||
| 86 | } | ||||||
| 87 | else { | ||||||
| 88 | |||||||
| 89 | # Saying it isn't implemented is better than crashing! | ||||||
| 90 | $response->code(501); | ||||||
| 91 | $response->message('Not Implemented'); | ||||||
| 92 | } | ||||||
| 93 | return $response; | ||||||
| 94 | } | ||||||
| 95 | |||||||
| 96 | sub options { | ||||||
| 97 | my ( $self, $request, $response ) = @_; | ||||||
| 98 |      $response->header( 'DAV'           => '1,2, | 
||||||
| 99 | $response->header( 'MS-Author-Via' => 'DAV' ); # Nautilus freaks out | ||||||
| 100 | $response->header( 'Allow' => join( ',', map { uc } keys %implemented ) ); | ||||||
| 101 | $response->header( 'Content-Type' => 'httpd/unix-directory' ); | ||||||
| 102 | $response->header( 'Keep-Alive' => 'timeout=15, max=96' ); | ||||||
| 103 | return $response; | ||||||
| 104 | } | ||||||
| 105 | |||||||
| 106 | sub head { | ||||||
| 107 | my ( $self, $request, $response ) = @_; | ||||||
| 108 | my $path = uri_unescape $request->uri->path; | ||||||
| 109 | my $fs = $self->filesys; | ||||||
| 110 | |||||||
| 111 | if ( $fs->test( 'f', $path ) && $fs->test( 'r', $path ) ) { | ||||||
| 112 | $response->last_modified( $fs->modtime($path) ); | ||||||
| 113 | } | ||||||
| 114 | elsif ( $fs->test( 'd', $path ) ) { | ||||||
| 115 | $response->header( 'Content-Type' => 'text/html; charset="utf-8"' ); | ||||||
| 116 | } | ||||||
| 117 | else { | ||||||
| 118 | $response = HTTP::Response->new( 404, 'NOT FOUND', $response->headers ); | ||||||
| 119 | } | ||||||
| 120 | return $response; | ||||||
| 121 | } | ||||||
| 122 | |||||||
| 123 | sub get { | ||||||
| 124 | my ( $self, $request, $response ) = @_; | ||||||
| 125 | my $path = uri_unescape $request->uri->path; | ||||||
| 126 | my $fs = $self->filesys; | ||||||
| 127 | |||||||
| 128 | if ( $fs->test( 'f', $path ) && $fs->test( 'r', $path ) ) { | ||||||
| 129 | my $fh = $fs->open_read($path); | ||||||
| 130 | my $file = join '', <$fh>; | ||||||
| 131 | $fs->close_read($fh); | ||||||
| 132 | $response->content($file); | ||||||
| 133 | $response->last_modified( $fs->modtime($path) ); | ||||||
| 134 | } | ||||||
| 135 | elsif ( $fs->test( 'd', $path ) ) { | ||||||
| 136 | |||||||
| 137 | # a web browser, then | ||||||
| 138 | my @files = $fs->list($path); | ||||||
| 139 | my $body; | ||||||
| 140 | my $fpath = $path =~ m{/$} ? $path : $path . '/'; | ||||||
| 141 | foreach my $file (@files) { | ||||||
| 142 | if ( $fs->test( 'd', $fpath . $file ) ) { | ||||||
| 143 |                  $body .= qq|$file/ \n|;  | 
||||||
| 144 | } | ||||||
| 145 | else { | ||||||
| 146 | $file =~ s{/$}{}; | ||||||
| 147 |                  $body .= qq|$file \n|;  | 
||||||
| 148 | } | ||||||
| 149 | } | ||||||
| 150 | $response->header( 'Content-Type' => 'text/html; charset="utf-8"' ); | ||||||
| 151 | $response->content($body); | ||||||
| 152 | } | ||||||
| 153 | else { | ||||||
| 154 | return HTTP::Response->new( 404, 'Not Found' ); | ||||||
| 155 | } | ||||||
| 156 | return $response; | ||||||
| 157 | } | ||||||
| 158 | |||||||
| 159 | sub _lock_manager { | ||||||
| 160 | my ($self) = @_; | ||||||
| 161 | unless ( $self->{'lock_manager'} ) { | ||||||
| 162 | if ( $self->{'_dsn'} ) { | ||||||
| 163 | my $db = Net::DAV::LockManager::DB->new( $self->{'_dsn'} ); | ||||||
| 164 | $self->{'lock_manager'} = Net::DAV::LockManager->new($db); | ||||||
| 165 | } | ||||||
| 166 | else { | ||||||
| 167 | $self->{'lock_manager'} = Net::DAV::LockManager->new(); | ||||||
| 168 | } | ||||||
| 169 | } | ||||||
| 170 | return $self->{'lock_manager'}; | ||||||
| 171 | } | ||||||
| 172 | |||||||
| 173 | sub lock { | ||||||
| 174 | my ( $self, $request, $response ) = @_; | ||||||
| 175 | |||||||
| 176 | my $lockreq = _parse_lock_request($request); | ||||||
| 177 | |||||||
| 178 | # Invalid XML requires a 400 response code. | ||||||
| 179 | return HTTP::Response->new( 400, 'Bad Request' ) unless defined $lockreq; | ||||||
| 180 | |||||||
| 181 | if ( !$lockreq->{'has_content'} ) { | ||||||
| 182 | |||||||
| 183 | # Not already locked. | ||||||
| 184 | return HTTP::Response->new( 403, 'Forbidden' ) if !$lockreq->{'token'}; | ||||||
| 185 | |||||||
| 186 | # Reset timeout | ||||||
| 187 | if ( my $lock = $self->_lock_manager()->refresh_lock($lockreq) ) { | ||||||
| 188 | $response->header( 'Content-Type' => 'text/xml; charset="utf-8"' ); | ||||||
| 189 | $response->content( | ||||||
| 190 | _lock_response_content( | ||||||
| 191 | { | ||||||
| 192 | 'path' => $lock->path, | ||||||
| 193 | 'token' => $lock->token, | ||||||
| 194 | 'timeout' => $lock->timeout, | ||||||
| 195 | 'scope' => $lock->scope, | ||||||
| 196 | 'depth' => $lock->depth, | ||||||
| 197 | } | ||||||
| 198 | ) | ||||||
| 199 | ); | ||||||
| 200 | } | ||||||
| 201 | else { | ||||||
| 202 | my $curr = $self->_lock_manager()->find_lock( { 'path' => $lockreq->{'path'} } ); | ||||||
| 203 | return HTTP::Response->new( 412, 'Precondition Failed' ) unless $curr; | ||||||
| 204 | |||||||
| 205 | # Not the correct lock token | ||||||
| 206 | return HTTP::Response->new( 412, 'Precondition Failed' ) if $lockreq->{'token'} ne $curr->token; | ||||||
| 207 | |||||||
| 208 | # Not the correct user. | ||||||
| 209 | return HTTP::Response->new( 403, 'Forbidden' ); | ||||||
| 210 | } | ||||||
| 211 | return $response; | ||||||
| 212 | } | ||||||
| 213 | |||||||
| 214 | # Validate depth request | ||||||
| 215 | return HTTP::Response->new( 400, 'Bad Request' ) unless $lockreq->{'depth'} =~ /^(?:0|infinity)$/; | ||||||
| 216 | |||||||
| 217 | my $lock = $self->_lock_manager()->lock($lockreq); | ||||||
| 218 | |||||||
| 219 | if ( !$lock ) { | ||||||
| 220 | my $curr = $self->_lock_manager()->find_lock( { 'path' => $lockreq->{'path'} } ); | ||||||
| 221 | return HTTP::Response->new( 412, 'Precondition Failed' ) unless $curr; | ||||||
| 222 | |||||||
| 223 | # Not the correct lock token | ||||||
| 224 | return HTTP::Response->new( 412, 'Precondition Failed' ) if $lockreq->{'token'}||'' ne $curr->token; | ||||||
| 225 | |||||||
| 226 | # Resource is already locked | ||||||
| 227 | return HTTP::Response->new( 403, 'Forbidden' ); | ||||||
| 228 | } | ||||||
| 229 | |||||||
| 230 | my $token = $lock->token; | ||||||
| 231 | $response->code( 200 ); | ||||||
| 232 | $response->message( 'OK' ); | ||||||
| 233 | $response->header( 'Lock-Token', "<$token>" ); | ||||||
| 234 | $response->header( 'Content-Type', 'text/xml; charset="utf-8"' ); | ||||||
| 235 | $response->content( | ||||||
| 236 | _lock_response_content( | ||||||
| 237 | { | ||||||
| 238 | 'path' => $lock->path, | ||||||
| 239 | 'token' => $token, | ||||||
| 240 | 'timeout' => $lock->timeout, | ||||||
| 241 | 'scope' => 'exclusive', | ||||||
| 242 | 'depth' => $lock->depth, | ||||||
| 243 | 'owner_node' => $lockreq->{'owner_node'}, | ||||||
| 244 | } | ||||||
| 245 | ) | ||||||
| 246 | ); | ||||||
| 247 | |||||||
| 248 | # Create empty file if none exists, as per RFC 4918, Section 9.10.4 | ||||||
| 249 | my $fs = $self->filesys; | ||||||
| 250 | if ( !$fs->test( 'e', $lock->path ) ) { | ||||||
| 251 | my $fh = $fs->open_write( $lock->path, 1 ); | ||||||
| 252 | $fs->close_write($fh) if $fh; | ||||||
| 253 | } | ||||||
| 254 | |||||||
| 255 | return $response; | ||||||
| 256 | } | ||||||
| 257 | |||||||
| 258 | sub _get_timeout { | ||||||
| 259 | my ($to_header) = @_; | ||||||
| 260 | return undef unless defined $to_header and length $to_header; | ||||||
| 261 | |||||||
| 262 | my @timeouts = sort | ||||||
| 263 | map { /Second-(\d+)/ ? $1 : $_ } | ||||||
| 264 | grep { $_ ne 'Infinite' } | ||||||
| 265 | split /\s*,\s*/, $to_header; | ||||||
| 266 | |||||||
| 267 | return undef unless @timeouts; | ||||||
| 268 | return $timeouts[0]; | ||||||
| 269 | } | ||||||
| 270 | |||||||
| 271 | sub _parse_lock_header { | ||||||
| 272 | my ($req) = @_; | ||||||
| 273 | my $depth = $req->header('Depth'); | ||||||
| 274 | my %lockreq = ( | ||||||
| 275 | 'path' => uri_unescape( $req->uri->path ), | ||||||
| 276 | |||||||
| 277 | # Assuming basic auth for now. | ||||||
| 278 | 'user' => ( $req->authorization_basic() )[0] || '', | ||||||
| 279 | 'token' => ( _extract_lock_token($req) || undef ), | ||||||
| 280 | 'timeout' => _get_timeout( $req->header('Timeout') ), | ||||||
| 281 | 'depth' => ( defined $depth ? $depth : 'infinity' ), | ||||||
| 282 | ); | ||||||
| 283 | return \%lockreq; | ||||||
| 284 | } | ||||||
| 285 | |||||||
| 286 | sub _parse_lock_request { | ||||||
| 287 | my ($req) = @_; | ||||||
| 288 | my $lockreq = _parse_lock_header($req); | ||||||
| 289 | return $lockreq unless $req->content; | ||||||
| 290 | |||||||
| 291 | my $parser = XML::LibXML->new; | ||||||
| 292 | my $doc; | ||||||
| 293 | eval { $doc = $parser->parse_string( $req->content ); } or do { | ||||||
| 294 | |||||||
| 295 | # Request body must be a valid XML request | ||||||
| 296 | return; | ||||||
| 297 | }; | ||||||
| 298 | my $xpc = XML::LibXML::XPathContext->new($doc); | ||||||
| 299 | $xpc->registerNs( 'D', 'DAV:' ); | ||||||
| 300 | |||||||
| 301 | # Want the following in list context. | ||||||
| 302 | $lockreq->{'owner_node'} = ( $xpc->findnodes('/D:lockinfo/D:owner') )[0]; | ||||||
| 303 | if ( $lockreq->{'owner_node'} ) { | ||||||
| 304 | my $owner = $lockreq->{'owner_node'}->toString; | ||||||
| 305 | $owner =~ s/^<(?:[^:]+:)?owner>//sm; | ||||||
| 306 | $owner =~ s!(?:[^:]+:)?owner>$!!sm; | ||||||
| 307 | $lockreq->{'owner'} = $owner; | ||||||
| 308 | } | ||||||
| 309 | $lockreq->{'scope'} = eval { ( $xpc->findnodes('/D:lockinfo/D:lockscope/D:*') )[0]->localname; }; | ||||||
| 310 | $lockreq->{'has_content'} = 1; | ||||||
| 311 | |||||||
| 312 | return $lockreq; | ||||||
| 313 | } | ||||||
| 314 | |||||||
| 315 | sub _extract_lock_token { | ||||||
| 316 | my ($req) = @_; | ||||||
| 317 | my $token = $req->header('If'); | ||||||
| 318 | unless ($token) { | ||||||
| 319 | $token = $req->header('Lock-Token'); | ||||||
| 320 | return $1 if defined $token && $token =~ /<([^>]+)>/; | ||||||
| 321 | return undef; | ||||||
| 322 | } | ||||||
| 323 | |||||||
| 324 | # Based on the last paragraph of section 10.4.1 of RFC 4918, it appears | ||||||
| 325 | # that any lock token that appears in the If header is available as a | ||||||
| 326 | # known lock token. Rather than trying to deal with the whole entity, | ||||||
| 327 | # lock, implicit and/or, and Not (with and without resources) thing, | ||||||
| 328 | # This code just returns a list of lock tokens found in the header. | ||||||
| 329 | my @tokens = map { $_ =~ /<([^>]+)>/g } ( $token =~ /\(([^\)]+)\)/g ); | ||||||
| 330 | |||||||
| 331 | return undef unless @tokens; | ||||||
| 332 | return @tokens == 1 ? $tokens[0] : \@tokens; | ||||||
| 333 | } | ||||||
| 334 | |||||||
| 335 | sub _lock_response_content { | ||||||
| 336 | my ($args) = @_; | ||||||
| 337 | my $resp = XML::LibXML::Document->new( '1.0', 'utf-8' ); | ||||||
| 338 | my $prop = _dav_root( $resp, 'prop' ); | ||||||
| 339 | my $lock = _dav_child( _dav_child( $prop, 'lockdiscovery' ), 'activelock' ); | ||||||
| 340 | _dav_child( _dav_child( $lock, 'locktype' ), 'write' ); | ||||||
| 341 | _dav_child( _dav_child( $lock, 'lockscope' ), $args->{'scope'} || 'exclusive' ); | ||||||
| 342 | _dav_child( $lock, 'depth', $args->{'depth'} || 'infinity' ); | ||||||
| 343 | if ( $args->{'owner_node'} ) { | ||||||
| 344 | my $owner = $args->{'owner_node'}->cloneNode(1); | ||||||
| 345 | $resp->adoptNode($owner); | ||||||
| 346 | $lock->addChild($owner); | ||||||
| 347 | } | ||||||
| 348 | _dav_child( $lock, 'timeout', "Second-$args->{'timeout'}" ); | ||||||
| 349 | _dav_child( _dav_child( $lock, 'locktoken' ), 'href', $args->{'token'} ); | ||||||
| 350 | _dav_child( _dav_child( $lock, 'lockroot' ), 'href', $args->{'path'} ); | ||||||
| 351 | |||||||
| 352 | return $resp->toString; | ||||||
| 353 | } | ||||||
| 354 | |||||||
| 355 | sub _active_lock_prop { | ||||||
| 356 | my ( $doc, $lock ) = @_; | ||||||
| 357 | my $active = $doc->createElement('D:activelock'); | ||||||
| 358 | |||||||
| 359 | # All locks are write | ||||||
| 360 | _dav_child( _dav_child( $active, 'locktype' ), 'write' ); | ||||||
| 361 | _dav_child( _dav_child( $active, 'lockscope' ), $lock->scope ); | ||||||
| 362 | _dav_child( $active, 'depth', $lock->depth ); | ||||||
| 363 |      $active->appendWellBalancedChunk( ' | 
||||||
| 364 | _dav_child( $active, 'timeout', 'Second-' . $lock->timeout ); | ||||||
| 365 | _dav_child( _dav_child( $active, 'locktoken' ), 'href', $lock->token ); | ||||||
| 366 | _dav_child( _dav_child( $active, 'lockroot' ), 'href', $lock->path ); | ||||||
| 367 | |||||||
| 368 | return $active; | ||||||
| 369 | } | ||||||
| 370 | |||||||
| 371 | sub unlock { | ||||||
| 372 | my ( $self, $request, $response ) = @_; | ||||||
| 373 | my $path = uri_unescape( $request->uri->path ); | ||||||
| 374 | my $lockreq = _parse_lock_header($request); | ||||||
| 375 | |||||||
| 376 | # No lock token supplied, we cannot unlock | ||||||
| 377 | return HTTP::Response->new( 400, 'Bad Request' ) unless $lockreq->{'token'}; | ||||||
| 378 | |||||||
| 379 | if ( !$self->_lock_manager()->unlock($lockreq) ) { | ||||||
| 380 | my $curr = $self->_lock_manager()->find_lock( { 'path' => $lockreq->{'path'} } ); | ||||||
| 381 | |||||||
| 382 | # No lock exists, conflicting requirements. | ||||||
| 383 | return HTTP::Response->new( 409, 'Conflict' ) unless $curr; | ||||||
| 384 | |||||||
| 385 | # Not the owner of the lock or bad token. | ||||||
| 386 | return HTTP::Response->new( 403, 'Forbidden' ); | ||||||
| 387 | } | ||||||
| 388 | |||||||
| 389 | return HTTP::Response->new( 204, 'No content' ); | ||||||
| 390 | } | ||||||
| 391 | |||||||
| 392 | sub _dav_child { | ||||||
| 393 | my ( $parent, $tag, $text ) = @_; | ||||||
| 394 | my $child = $parent->ownerDocument->createElement("D:$tag"); | ||||||
| 395 | $parent->addChild($child); | ||||||
| 396 | $child->appendText($text) if defined $text; | ||||||
| 397 | return $child; | ||||||
| 398 | } | ||||||
| 399 | |||||||
| 400 | sub _dav_root { | ||||||
| 401 | my ( $doc, $tag ) = @_; | ||||||
| 402 | my $root = $doc->createElementNS( 'DAV:', $tag ); | ||||||
| 403 | $root->setNamespace( 'DAV:', 'D', 1 ); | ||||||
| 404 | $doc->setDocumentElement($root); | ||||||
| 405 | return $root; | ||||||
| 406 | } | ||||||
| 407 | |||||||
| 408 | sub _can_modify { | ||||||
| 409 | my ( $self, $request ) = @_; | ||||||
| 410 | my $lockreq = _parse_lock_header($request); | ||||||
| 411 | return $self->_lock_manager()->can_modify($lockreq); | ||||||
| 412 | } | ||||||
| 413 | |||||||
| 414 | sub post { | ||||||
| 415 | my ( $self, $request, $response ) = @_; | ||||||
| 416 | |||||||
| 417 | if ( !$self->_can_modify( $request ) ) { | ||||||
| 418 | return HTTP::Response->new( 403, 'Forbidden' ); | ||||||
| 419 | } | ||||||
| 420 | |||||||
| 421 | return HTTP::Response->new( 501, 'Not Implemented' ); | ||||||
| 422 | } | ||||||
| 423 | |||||||
| 424 | sub put { | ||||||
| 425 | my ( $self, $request, $response ) = @_; | ||||||
| 426 | |||||||
| 427 | if ( !$self->_can_modify($request) ) { | ||||||
| 428 | return HTTP::Response->new( 403, 'Forbidden' ); | ||||||
| 429 | } | ||||||
| 430 | |||||||
| 431 | my $path = uri_unescape $request->uri->path; | ||||||
| 432 | my $fs = $self->filesys; | ||||||
| 433 | |||||||
| 434 | return HTTP::Response->new( 405, 'Method Not Allowed' ) if $fs->test( 'd', $path ); | ||||||
| 435 | my $parent = $path; | ||||||
| 436 | $parent =~ s{/[^/]+$}{}; | ||||||
| 437 | $parent = '/' if $parent eq ''; | ||||||
| 438 | # Parent directory does not exist. | ||||||
| 439 | return HTTP::Response->new( 409, 'Conflict' ) unless $fs->test( 'd', $parent ); | ||||||
| 440 | |||||||
| 441 | my $fh = $fs->open_write( $path ); | ||||||
| 442 | if ( $fh ) { | ||||||
| 443 | $response = HTTP::Response->new( 201, 'Created', $response->headers ); | ||||||
| 444 | print $fh $request->content; | ||||||
| 445 | $fs->close_write($fh); | ||||||
| 446 | } | ||||||
| 447 | else { | ||||||
| 448 | # Unable to write for some other reason. | ||||||
| 449 | return HTTP::Response->new( 403, 'Forbidden' ); | ||||||
| 450 | } | ||||||
| 451 | |||||||
| 452 | return $response; | ||||||
| 453 | } | ||||||
| 454 | |||||||
| 455 | sub _delete_xml { | ||||||
| 456 | my ( $dom, $path ) = @_; | ||||||
| 457 | |||||||
| 458 | my $response = $dom->createElement('d:response'); | ||||||
| 459 | $response->appendTextChild( 'd:href' => $path ); | ||||||
| 460 | $response->appendTextChild( 'd:status' => 'HTTP/1.1 401 Permission Denied' ); # *** FIXME *** | ||||||
| 461 | } | ||||||
| 462 | |||||||
| 463 | sub delete { | ||||||
| 464 | my ( $self, $request, $response ) = @_; | ||||||
| 465 | |||||||
| 466 | if ( !$self->_can_modify($request) ) { | ||||||
| 467 | return HTTP::Response->new( 403, 'Forbidden' ); | ||||||
| 468 | } | ||||||
| 469 | |||||||
| 470 | if ( $request->uri->fragment ) { | ||||||
| 471 | return HTTP::Response->new( 404, 'Not Found', $response->headers ); | ||||||
| 472 | } | ||||||
| 473 | |||||||
| 474 | my $path = uri_unescape $request->uri->path; | ||||||
| 475 | my $fs = $self->filesys; | ||||||
| 476 | unless ( $fs->test( 'e', $path ) ) { | ||||||
| 477 | return HTTP::Response->new( 404, 'Not Found', $response->headers ); | ||||||
| 478 | } | ||||||
| 479 | |||||||
| 480 | my $dom = XML::LibXML::Document->new( '1.0', 'utf-8' ); | ||||||
| 481 | my @error; | ||||||
| 482 | # see rt 46865: files first since rmdir() only removed empty directories | ||||||
| 483 | foreach my $part ( _get_files($fs, $path), _get_dirs($fs, $path), $path ) { | ||||||
| 484 | |||||||
| 485 | next unless $fs->test( 'e', $part ); | ||||||
| 486 | |||||||
| 487 | if ( $fs->test( 'f', $part ) ) { | ||||||
| 488 | push @error, _delete_xml( $dom, $part ) | ||||||
| 489 | unless $fs->delete($part); | ||||||
| 490 | } | ||||||
| 491 | elsif ( $fs->test( 'd', $part ) ) { | ||||||
| 492 | push @error, _delete_xml( $dom, $part ) | ||||||
| 493 | unless $fs->rmdir($part); | ||||||
| 494 | } | ||||||
| 495 | } | ||||||
| 496 | |||||||
| 497 | if (@error) { | ||||||
| 498 | my $multistatus = $dom->createElement('D:multistatus'); | ||||||
| 499 | $multistatus->setAttribute( 'xmlns:D', 'DAV:' ); | ||||||
| 500 | |||||||
| 501 | $multistatus->addChild($_) foreach @error; | ||||||
| 502 | |||||||
| 503 | $response = HTTP::Response->new( 207 => 'Multi-Status' ); | ||||||
| 504 | $response->header( 'Content-Type' => 'text/xml; charset="utf-8"' ); | ||||||
| 505 | } | ||||||
| 506 | else { | ||||||
| 507 | $response = HTTP::Response->new( 204 => 'No Content' ); | ||||||
| 508 | } | ||||||
| 509 | return $response; | ||||||
| 510 | } | ||||||
| 511 | |||||||
| 512 | sub copy { | ||||||
| 513 | my ( $self, $request, $response ) = @_; | ||||||
| 514 | my $path = uri_unescape $request->uri->path; | ||||||
| 515 | $path =~ s{/+$}{}; # see rt 46865 | ||||||
| 516 | |||||||
| 517 | # need to modify request to pay attention to destination address. | ||||||
| 518 | my $lockreq = _parse_lock_header( $request ); | ||||||
| 519 | $lockreq->{'path'} = uri_unescape( $request->header( 'Destination' ) ); | ||||||
| 520 | if ( !$self->_lock_manager()->can_modify( $lockreq ) ) { | ||||||
| 521 | return HTTP::Response->new( 403, 'Forbidden' ); | ||||||
| 522 | } | ||||||
| 523 | my $fs = $self->filesys; | ||||||
| 524 | |||||||
| 525 | my $destination = $request->header('Destination'); | ||||||
| 526 | $destination = URI->new($destination)->path; | ||||||
| 527 | $destination =~ s{/+$}{}; # see rt 46865 | ||||||
| 528 | |||||||
| 529 | my $depth = $request->header('Depth'); | ||||||
| 530 | $depth = '' if !defined $depth; | ||||||
| 531 | |||||||
| 532 | my $overwrite = $request->header('Overwrite') || 'F'; | ||||||
| 533 | |||||||
| 534 | if ( $fs->test( "f", $path ) ) { | ||||||
| 535 | return $self->_copy_file( $request, $response ); | ||||||
| 536 | } | ||||||
| 537 | |||||||
| 538 | my @files = _get_files($fs, $path, $depth); | ||||||
| 539 | my @dirs = _get_dirs($fs, $path, $depth); | ||||||
| 540 | |||||||
| 541 | push @dirs, $path; | ||||||
| 542 | foreach my $dir ( sort @dirs ) { | ||||||
| 543 | my $destdir = $dir; | ||||||
| 544 | $destdir =~ s/^$path/$destination/; | ||||||
| 545 | if ( $overwrite eq 'F' && $fs->test( "e", $destdir ) ) { | ||||||
| 546 | return HTTP::Response->new( 401, "ERROR", $response->headers ); | ||||||
| 547 | } | ||||||
| 548 | $fs->mkdir($destdir); | ||||||
| 549 | } | ||||||
| 550 | |||||||
| 551 | foreach my $file ( reverse sort @files ) { | ||||||
| 552 | my $destfile = $file; | ||||||
| 553 | $destfile =~ s/^$path/$destination/; | ||||||
| 554 | my $fh = $fs->open_read($file); | ||||||
| 555 | my $file = join '', <$fh>; | ||||||
| 556 | $fs->close_read($fh); | ||||||
| 557 | if ( $fs->test( 'e', $destfile ) ) { | ||||||
| 558 | if ( $overwrite eq 'T' ) { | ||||||
| 559 | $fh = $fs->open_write($destfile); | ||||||
| 560 | print $fh $file; | ||||||
| 561 | $fs->close_write($fh); | ||||||
| 562 | } | ||||||
| 563 | else { | ||||||
| 564 | return HTTP::Response( 412, 'Precondition Failed' ); | ||||||
| 565 | } | ||||||
| 566 | } | ||||||
| 567 | else { | ||||||
| 568 | $fh = $fs->open_write($destfile); | ||||||
| 569 | print $fh $file; | ||||||
| 570 | $fs->close_write($fh); | ||||||
| 571 | } | ||||||
| 572 | } | ||||||
| 573 | |||||||
| 574 | $response = HTTP::Response->new( 200, 'OK', $response->headers ); | ||||||
| 575 | return $response; | ||||||
| 576 | } | ||||||
| 577 | |||||||
| 578 | sub _copy_file { | ||||||
| 579 | my ( $self, $request, $response ) = @_; | ||||||
| 580 | my $path = uri_unescape $request->uri->path; | ||||||
| 581 | my $fs = $self->filesys; | ||||||
| 582 | |||||||
| 583 | my $destination = $request->header('Destination'); | ||||||
| 584 | $destination = URI->new($destination)->path; | ||||||
| 585 | my $depth = $request->header('Depth'); | ||||||
| 586 | my $overwrite = $request->header('Overwrite'); | ||||||
| 587 | |||||||
| 588 | if ( $fs->test( 'd', $destination ) ) { | ||||||
| 589 | return HTTP::Response->new( 204, 'No Content', $response->headers ); | ||||||
| 590 | } | ||||||
| 591 | if ( $fs->test( 'f', $path ) && $fs->test( 'r', $path ) ) { | ||||||
| 592 | my $fh = $fs->open_read($path); | ||||||
| 593 | my $file = join '', <$fh>; | ||||||
| 594 | $fs->close_read($fh); | ||||||
| 595 | if ( $fs->test( 'f', $destination ) ) { | ||||||
| 596 | if ( $overwrite eq 'T' ) { | ||||||
| 597 | $fh = $fs->open_write($destination); | ||||||
| 598 | print $fh $file; | ||||||
| 599 | $fs->close_write($fh); | ||||||
| 600 | } | ||||||
| 601 | else { | ||||||
| 602 | return HTTP::Response( 412, 'Precondition Failed' ); | ||||||
| 603 | } | ||||||
| 604 | } | ||||||
| 605 | else { | ||||||
| 606 | unless ( $fh = $fs->open_write($destination) ) { | ||||||
| 607 | return HTTP::Response->new( 409, 'Conflict' ); | ||||||
| 608 | } | ||||||
| 609 | print $fh $file; | ||||||
| 610 | $fs->close_write($fh); | ||||||
| 611 | $response->code(201); | ||||||
| 612 | $response->message('Created'); | ||||||
| 613 | } | ||||||
| 614 | } | ||||||
| 615 | else { | ||||||
| 616 | return HTTP::Response->new( 404, 'Not Found' ); | ||||||
| 617 | } | ||||||
| 618 | |||||||
| 619 | return $response; | ||||||
| 620 | } | ||||||
| 621 | |||||||
| 622 | sub move { | ||||||
| 623 | my ( $self, $request, $response ) = @_; | ||||||
| 624 | |||||||
| 625 | # need to check both paths for locks. | ||||||
| 626 | my $lockreq = _parse_lock_header( $request ); | ||||||
| 627 | if ( !$self->_lock_manager()->can_modify( $lockreq ) ) { | ||||||
| 628 | return HTTP::Response->new( 403, 'Forbidden' ); | ||||||
| 629 | } | ||||||
| 630 | $lockreq->{'path'} = uri_unescape( $request->header( 'Destination' ) ); | ||||||
| 631 | if ( !$self->_lock_manager()->can_modify( $lockreq ) ) { | ||||||
| 632 | return HTTP::Response->new( 403, 'Forbidden' ); | ||||||
| 633 | } | ||||||
| 634 | |||||||
| 635 | my $destination = $request->header('Destination'); | ||||||
| 636 | $destination = URI->new($destination)->path; | ||||||
| 637 | my $destexists = $self->filesys->test( "e", $destination ); | ||||||
| 638 | |||||||
| 639 | $response = $self->copy( $request, $response ); | ||||||
| 640 | $response = $self->delete( $request, $response ) | ||||||
| 641 | if $response->is_success; | ||||||
| 642 | |||||||
| 643 | $response->code(201) unless $destexists; | ||||||
| 644 | |||||||
| 645 | return $response; | ||||||
| 646 | } | ||||||
| 647 | |||||||
| 648 | sub mkcol { | ||||||
| 649 | my ( $self, $request, $response ) = @_; | ||||||
| 650 | my $path = uri_unescape $request->uri->path; | ||||||
| 651 | |||||||
| 652 | if ( !$self->_can_modify($request) ) { | ||||||
| 653 | return HTTP::Response->new( 403, 'Forbidden' ); | ||||||
| 654 | } | ||||||
| 655 | |||||||
| 656 | my $fs = $self->filesys; | ||||||
| 657 | |||||||
| 658 | return HTTP::Response->new( 415, 'Unsupported Media Type' ) if $request->content; | ||||||
| 659 | return HTTP::Response->new( 405, 'Method Not Allowed' ) if $fs->test( 'e', $path ); | ||||||
| 660 | $fs->mkdir($path); | ||||||
| 661 | if ( $fs->test( 'd', $path ) ) { | ||||||
| 662 | $response->code(201); | ||||||
| 663 | $response->message('Created'); | ||||||
| 664 | } | ||||||
| 665 | else { | ||||||
| 666 | $response->code(409); | ||||||
| 667 | $response->message('Conflict'); | ||||||
| 668 | } | ||||||
| 669 | |||||||
| 670 | return $response; | ||||||
| 671 | } | ||||||
| 672 | |||||||
| 673 | sub propfind { | ||||||
| 674 | my ( $self, $request, $response ) = @_; | ||||||
| 675 | my $path = uri_unescape $request->uri->path; | ||||||
| 676 | my $fs = $self->filesys; | ||||||
| 677 | my $depth = $request->header('Depth'); | ||||||
| 678 | |||||||
| 679 | my $reqinfo = 'allprop'; | ||||||
| 680 | my @reqprops; | ||||||
| 681 | if ( $request->header('Content-Length') ) { | ||||||
| 682 | my $content = $request->content; | ||||||
| 683 | my $parser = XML::LibXML->new; | ||||||
| 684 | my $doc; | ||||||
| 685 | eval { $doc = $parser->parse_string($content); }; | ||||||
| 686 | if ($@) { | ||||||
| 687 | return HTTP::Response->new( 400, 'Bad Request' ); | ||||||
| 688 | } | ||||||
| 689 | |||||||
| 690 | #$reqinfo = doc->find('/DAV:propfind/*')->localname; | ||||||
| 691 | $reqinfo = $doc->find('/*/*')->shift->localname; | ||||||
| 692 | if ( $reqinfo eq 'prop' ) { | ||||||
| 693 | |||||||
| 694 | #for my $node ($doc->find('/DAV:propfind/DAV:prop/*')) { | ||||||
| 695 | for my $node ( $doc->find('/*/*/*')->get_nodelist ) { | ||||||
| 696 | push @reqprops, [ $node->namespaceURI, $node->localname ]; | ||||||
| 697 | } | ||||||
| 698 | } | ||||||
| 699 | } | ||||||
| 700 | |||||||
| 701 | if ( !$fs->test( 'e', $path ) ) { | ||||||
| 702 | return HTTP::Response->new( 404, 'Not Found' ); | ||||||
| 703 | } | ||||||
| 704 | |||||||
| 705 | $response->code(207); | ||||||
| 706 | $response->message('Multi-Status'); | ||||||
| 707 | $response->header( 'Content-Type' => 'text/xml; charset="utf-8"' ); | ||||||
| 708 | |||||||
| 709 | my $doc = XML::LibXML::Document->new( '1.0', 'utf-8' ); | ||||||
| 710 | my $multistat = $doc->createElement('D:multistatus'); | ||||||
| 711 | $multistat->setAttribute( 'xmlns:D', 'DAV:' ); | ||||||
| 712 | $doc->setDocumentElement($multistat); | ||||||
| 713 | |||||||
| 714 | my @paths; | ||||||
| 715 | if ( defined $depth && $depth eq 1 and $fs->test( 'd', $path ) ) { | ||||||
| 716 | my $p = $path; | ||||||
| 717 | $p .= '/' unless $p =~ m{/$}; | ||||||
| 718 | @paths = map { $p . $_ } File::Spec->no_upwards( $fs->list($path) ); | ||||||
| 719 | push @paths, $path; | ||||||
| 720 | } | ||||||
| 721 | else { | ||||||
| 722 | @paths = ($path); | ||||||
| 723 | } | ||||||
| 724 | |||||||
| 725 | for my $path (@paths) { | ||||||
| 726 | my ( | ||||||
| 727 | $dev, $ino, $mode, $nlink, $uid, $gid, $rdev, | ||||||
| 728 | $size, $atime, $mtime, $ctime, $blksize, $blocks | ||||||
| 729 | ) = $fs->stat($path); | ||||||
| 730 | |||||||
| 731 | # modified time is stringified human readable HTTP::Date style | ||||||
| 732 | $mtime = time2str($mtime); | ||||||
| 733 | |||||||
| 734 | # created time is ISO format | ||||||
| 735 | # tidy up date format - isoz isn't exactly what we want, but | ||||||
| 736 | # it's easy to change. | ||||||
| 737 | $ctime = time2isoz($ctime); | ||||||
| 738 | $ctime =~ s/ /T/; | ||||||
| 739 | $ctime =~ s/Z//; | ||||||
| 740 | |||||||
| 741 | $size ||= ''; | ||||||
| 742 | |||||||
| 743 | my $is_dir = $fs->test( 'd', $path ); | ||||||
| 744 | my $resp = _dav_child( $multistat, 'response' ); | ||||||
| 745 | my $href = File::Spec->catdir( | ||||||
| 746 | map { uri_escape $_} File::Spec->splitdir($path) | ||||||
| 747 | ) . ( $is_dir && $path !~ m{/$} ? '/' : ''); | ||||||
| 748 | $href =~ tr{\\}{/}; # Protection from wrong slashes under Windows. | ||||||
| 749 | _dav_child( $resp, 'href', $href ); | ||||||
| 750 | my $okprops = $doc->createElement('D:prop'); | ||||||
| 751 | my $nfprops = $doc->createElement('D:prop'); | ||||||
| 752 | my $prop; | ||||||
| 753 | |||||||
| 754 | if ( $reqinfo eq 'prop' ) { | ||||||
| 755 | my %prefixes = ( 'DAV:' => 'D' ); | ||||||
| 756 | my $i = 0; | ||||||
| 757 | |||||||
| 758 | for my $reqprop (@reqprops) { | ||||||
| 759 | my ( $ns, $name ) = @$reqprop; | ||||||
| 760 | if ( $ns eq 'DAV:' && $name eq 'creationdate' ) { | ||||||
| 761 | _dav_child( $okprops, 'creationdate', $ctime ); | ||||||
| 762 | } | ||||||
| 763 | elsif ( $ns eq 'DAV:' && $name eq 'getcontentlength' ) { | ||||||
| 764 | _dav_child( $okprops, 'getcontentlength', $is_dir ? () : ($size) ); | ||||||
| 765 | } | ||||||
| 766 | elsif ( $ns eq 'DAV:' && $name eq 'getcontenttype' ) { | ||||||
| 767 | _dav_child( $okprops, 'getcontenttype', $is_dir ? 'httpd/unix-directory' : 'httpd/unix-file' ); | ||||||
| 768 | } | ||||||
| 769 | elsif ( $ns eq 'DAV:' && $name eq 'getlastmodified' ) { | ||||||
| 770 | _dav_child( $okprops, 'getlastmodified', $mtime ); | ||||||
| 771 | } | ||||||
| 772 | elsif ( $ns eq 'DAV:' && $name eq 'resourcetype' ) { | ||||||
| 773 | $prop = _dav_child( $okprops, 'resourcetype' ); | ||||||
| 774 | if ( $is_dir ) { | ||||||
| 775 | _dav_child( $prop, 'collection' ); | ||||||
| 776 | } | ||||||
| 777 | } | ||||||
| 778 | elsif ( $ns eq 'DAV:' && $name eq 'lockdiscovery' ) { | ||||||
| 779 | $prop = _dav_child( $okprops, 'lockdiscovery' ); | ||||||
| 780 | my $user = ($request->authorization_basic())[0]||''; | ||||||
| 781 | foreach my $lock ( $self->_lock_manager()->list_all_locks({ 'path' => $path, 'user' => $user }) ) { | ||||||
| 782 | my $active = _active_lock_prop( $doc, $lock ); | ||||||
| 783 | $prop->addChild( $active ); | ||||||
| 784 | } | ||||||
| 785 | } | ||||||
| 786 | elsif ( $ns eq 'DAV:' && $name eq 'supportedlock' ) { | ||||||
| 787 | $prop = _supportedlock_child( $okprops ); | ||||||
| 788 | } | ||||||
| 789 | else { | ||||||
| 790 | my $prefix = $prefixes{$ns}; | ||||||
| 791 | if ( !defined $prefix ) { | ||||||
| 792 | $prefix = 'i' . $i++; | ||||||
| 793 | |||||||
| 794 |                          # mod_dav sets  | 
||||||
| 795 | #$nfprops->setAttribute("xmlns:$prefix", $ns); | ||||||
| 796 | $resp->setAttribute( "xmlns:$prefix", $ns ); | ||||||
| 797 | |||||||
| 798 | $prefixes{$ns} = $prefix; | ||||||
| 799 | } | ||||||
| 800 | |||||||
| 801 | $prop = $doc->createElement("$prefix:$name"); | ||||||
| 802 | $nfprops->addChild($prop); | ||||||
| 803 | } | ||||||
| 804 | } | ||||||
| 805 | } | ||||||
| 806 | elsif ( $reqinfo eq 'propname' ) { | ||||||
| 807 | _dav_child( $okprops, 'creationdate' ); | ||||||
| 808 | _dav_child( $okprops, 'getcontentlength' ); | ||||||
| 809 | _dav_child( $okprops, 'getcontenttype' ); | ||||||
| 810 | _dav_child( $okprops, 'getlastmodified' ); | ||||||
| 811 | _dav_child( $okprops, 'supportedlock' ); | ||||||
| 812 | _dav_child( $okprops, 'resourcetype' ); | ||||||
| 813 | } | ||||||
| 814 | else { | ||||||
| 815 | _dav_child( $okprops, 'creationdate', $ctime ); | ||||||
| 816 | _dav_child( $okprops, 'getcontentlength', $is_dir ? () : ($size) ); | ||||||
| 817 | _dav_child( $okprops, 'getcontenttype', $is_dir ? 'httpd/unix-directory' : 'httpd/unix-file' ); | ||||||
| 818 | _dav_child( $okprops, 'getlastmodified', $mtime ); | ||||||
| 819 | $prop = _supportedlock_child( $okprops ); | ||||||
| 820 | my $user = ($request->authorization_basic())[0]||''; | ||||||
| 821 | my @locks = $self->_lock_manager()->list_all_locks({ 'path' => $path, 'user' => $user }); | ||||||
| 822 | if ( @locks ) { | ||||||
| 823 | $prop = _dav_child( $okprops, 'lockdiscovery' ); | ||||||
| 824 | foreach my $lock ( @locks ) { | ||||||
| 825 | my $active = _active_lock_prop( $doc, $lock ); | ||||||
| 826 | $prop->addChild( $active ); | ||||||
| 827 | } | ||||||
| 828 | } | ||||||
| 829 | $prop = _dav_child( $okprops, 'resourcetype' ); | ||||||
| 830 | if ( $is_dir ) { | ||||||
| 831 | _dav_child( $prop, 'collection' ); | ||||||
| 832 | } | ||||||
| 833 | } | ||||||
| 834 | |||||||
| 835 | if ( $okprops->hasChildNodes ) { | ||||||
| 836 | my $propstat = _dav_child( $resp, 'propstat' ); | ||||||
| 837 | $propstat->addChild($okprops); | ||||||
| 838 | _dav_child( $propstat, 'status', 'HTTP/1.1 200 OK' ); | ||||||
| 839 | } | ||||||
| 840 | |||||||
| 841 | if ( $nfprops->hasChildNodes ) { | ||||||
| 842 | my $propstat = _dav_child( $resp, 'propstat' ); | ||||||
| 843 | $propstat->addChild($nfprops); | ||||||
| 844 | _dav_child( $propstat, 'status', 'HTTP/1.1 404 Not Found' ); | ||||||
| 845 | } | ||||||
| 846 | } | ||||||
| 847 | |||||||
| 848 | #this must be 0 as certin ms webdav clients choke on 1 | ||||||
| 849 | $response->content( $doc->toString(0) ); | ||||||
| 850 | |||||||
| 851 | return $response; | ||||||
| 852 | } | ||||||
| 853 | |||||||
| 854 | sub _supportedlock_child { | ||||||
| 855 | my ($okprops) = @_; | ||||||
| 856 | my $prop = _dav_child( $okprops, 'supportedlock' ); | ||||||
| 857 | #for my $n (qw(exclusive shared)) { # shared is currently not supported. | ||||||
| 858 | for my $n (qw(exclusive)) { | ||||||
| 859 | my $lock = _dav_child( $prop, 'lockentry' ); | ||||||
| 860 | |||||||
| 861 | _dav_child( _dav_child( $lock, 'lockscope' ), $n ); | ||||||
| 862 | _dav_child( _dav_child( $lock, 'locktype' ), 'write' ); | ||||||
| 863 | } | ||||||
| 864 | |||||||
| 865 | return $prop; | ||||||
| 866 | } | ||||||
| 867 | |||||||
| 868 | sub _get_files { | ||||||
| 869 | my ($fs, $path, $depth) = @_; | ||||||
| 870 | reverse map { s{/+}{/}g;s{/$}{}; $_ } | ||||||
| 871 | (defined $depth && $depth =~ m{\A\d+\z}) ? | ||||||
| 872 | File::Find::Rule::Filesys::Virtual->virtual($fs)->file->maxdepth($depth)->in($path) | ||||||
| 873 | : File::Find::Rule::Filesys::Virtual->virtual($fs)->file->in($path) | ||||||
| 874 | ; | ||||||
| 875 | } | ||||||
| 876 | |||||||
| 877 | sub _get_dirs { | ||||||
| 878 | my ($fs, $path, $depth) = @_; | ||||||
| 879 | return reverse sort | ||||||
| 880 | grep { $_ !~ m{/\.\.?$} } | ||||||
| 881 | map { s{/+}{/}g;s{/$}{}; $_ } | ||||||
| 882 | (defined $depth && $depth =~ m{\A\d+\z}) ? | ||||||
| 883 | File::Find::Rule::Filesys::Virtual->virtual($fs)->directory->maxdepth($depth)->in($path) | ||||||
| 884 | : File::Find::Rule::Filesys::Virtual->virtual($fs)->directory->in($path) | ||||||
| 885 | ; | ||||||
| 886 | } | ||||||
| 887 | |||||||
| 888 | 1; | ||||||
| 889 | |||||||
| 890 | __END__ |