File Coverage

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,' ); # Nautilus freaks out
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!$!!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( '' . $lock->owner . '' );
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 'xmlns' attribute - whatever
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__