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   699266 use strict;
  12         101  
  12         286  
3 12     12   52 use warnings;
  12         20  
  12         226  
4 12     12   3571 use File::Slurp;
  12         124658  
  12         674  
5 12     12   3946 use Encode;
  12         87072  
  12         755  
6 12     12   3028 use File::Find::Rule::Filesys::Virtual;
  12         115323  
  12         74  
7 12     12   4170 use HTTP::Date qw(time2str time2isoz);
  12         36873  
  12         630  
8 12     12   1779 use HTTP::Headers;
  12         33138  
  12         305  
9 12     12   1665 use HTTP::Response;
  12         85530  
  12         256  
10 12     12   1435 use HTTP::Request;
  12         4407  
  12         223  
11 12     12   59 use File::Spec;
  12         22  
  12         200  
12 12     12   48 use URI;
  12         19  
  12         195  
13 12     12   48 use URI::Escape;
  12         19  
  12         569  
14 12     12   837280 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.304';
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             foreach my $part (
483             grep { $_ !~ m{/\.\.?$} }
484             map { s{/+}{/}g; $_ }
485             File::Find::Rule::Filesys::Virtual->virtual($fs)->in($path), $path
486             ) {
487              
488             next unless $fs->test( 'e', $part );
489              
490             if ( $fs->test( 'f', $part ) ) {
491             push @error, _delete_xml( $dom, $part )
492             unless $fs->delete($part);
493             }
494             elsif ( $fs->test( 'd', $part ) ) {
495             push @error, _delete_xml( $dom, $part )
496             unless $fs->rmdir($part);
497             }
498             }
499              
500             if (@error) {
501             my $multistatus = $dom->createElement('D:multistatus');
502             $multistatus->setAttribute( 'xmlns:D', 'DAV:' );
503              
504             $multistatus->addChild($_) foreach @error;
505              
506             $response = HTTP::Response->new( 207 => 'Multi-Status' );
507             $response->header( 'Content-Type' => 'text/xml; charset="utf-8"' );
508             }
509             else {
510             $response = HTTP::Response->new( 204 => 'No Content' );
511             }
512             return $response;
513             }
514              
515             sub copy {
516             my ( $self, $request, $response ) = @_;
517             my $path = uri_unescape $request->uri->path;
518              
519             # need to modify request to pay attention to destination address.
520             my $lockreq = _parse_lock_header( $request );
521             $lockreq->{'path'} = uri_unescape( $request->header( 'Destination' ) );
522             if ( !$self->_lock_manager()->can_modify( $lockreq ) ) {
523             return HTTP::Response->new( 403, 'Forbidden' );
524             }
525             my $fs = $self->filesys;
526              
527             my $destination = $request->header('Destination');
528             $destination = URI->new($destination)->path;
529             my $depth = $request->header('Depth') || 0;
530             my $overwrite = $request->header('Overwrite') || 'F';
531              
532             if ( $fs->test( "f", $path ) ) {
533             return $self->_copy_file( $request, $response );
534             }
535              
536             # it's a good approximation
537             $depth = 100 if defined $depth && $depth eq 'infinity';
538              
539             my @files =
540             map { s{/+}{/}g; $_ }
541             File::Find::Rule::Filesys::Virtual->virtual($fs)->file->maxdepth($depth)
542             ->in($path);
543              
544             my @dirs = reverse sort
545             grep { $_ !~ m{/\.\.?$} }
546             map { s{/+}{/}g; $_ }
547             File::Find::Rule::Filesys::Virtual->virtual($fs)->directory->maxdepth($depth)
548             ->in($path);
549              
550             push @dirs, $path;
551             foreach my $dir ( sort @dirs ) {
552             my $destdir = $dir;
553             $destdir =~ s/^$path/$destination/;
554             if ( $overwrite eq 'F' && $fs->test( "e", $destdir ) ) {
555             return HTTP::Response->new( 401, "ERROR", $response->headers );
556             }
557             $fs->mkdir($destdir);
558             }
559              
560             foreach my $file ( reverse sort @files ) {
561             my $destfile = $file;
562             $destfile =~ s/^$path/$destination/;
563             my $fh = $fs->open_read($file);
564             my $file = join '', <$fh>;
565             $fs->close_read($fh);
566             if ( $fs->test( 'e', $destfile ) ) {
567             if ( $overwrite eq 'T' ) {
568             $fh = $fs->open_write($destfile);
569             print $fh $file;
570             $fs->close_write($fh);
571             }
572             else {
573             return HTTP::Response( 412, 'Precondition Failed' );
574             }
575             }
576             else {
577             $fh = $fs->open_write($destfile);
578             print $fh $file;
579             $fs->close_write($fh);
580             }
581             }
582              
583             $response = HTTP::Response->new( 200, 'OK', $response->headers );
584             return $response;
585             }
586              
587             sub _copy_file {
588             my ( $self, $request, $response ) = @_;
589             my $path = uri_unescape $request->uri->path;
590             my $fs = $self->filesys;
591              
592             my $destination = $request->header('Destination');
593             $destination = URI->new($destination)->path;
594             my $depth = $request->header('Depth');
595             my $overwrite = $request->header('Overwrite');
596              
597             if ( $fs->test( 'd', $destination ) ) {
598             return HTTP::Response->new( 204, 'No Content', $response->headers );
599             }
600             if ( $fs->test( 'f', $path ) && $fs->test( 'r', $path ) ) {
601             my $fh = $fs->open_read($path);
602             my $file = join '', <$fh>;
603             $fs->close_read($fh);
604             if ( $fs->test( 'f', $destination ) ) {
605             if ( $overwrite eq 'T' ) {
606             $fh = $fs->open_write($destination);
607             print $fh $file;
608             $fs->close_write($fh);
609             }
610             else {
611             return HTTP::Response( 412, 'Precondition Failed' );
612             }
613             }
614             else {
615             unless ( $fh = $fs->open_write($destination) ) {
616             return HTTP::Response->new( 409, 'Conflict' );
617             }
618             print $fh $file;
619             $fs->close_write($fh);
620             $response->code(201);
621             $response->message('Created');
622             }
623             }
624             else {
625             return HTTP::Response->new( 404, 'Not Found' );
626             }
627              
628             return $response;
629             }
630              
631             sub move {
632             my ( $self, $request, $response ) = @_;
633              
634             # need to check both paths for locks.
635             my $lockreq = _parse_lock_header( $request );
636             if ( !$self->_lock_manager()->can_modify( $lockreq ) ) {
637             return HTTP::Response->new( 403, 'Forbidden' );
638             }
639             $lockreq->{'path'} = uri_unescape( $request->header( 'Destination' ) );
640             if ( !$self->_lock_manager()->can_modify( $lockreq ) ) {
641             return HTTP::Response->new( 403, 'Forbidden' );
642             }
643              
644             my $destination = $request->header('Destination');
645             $destination = URI->new($destination)->path;
646             my $destexists = $self->filesys->test( "e", $destination );
647              
648             $response = $self->copy( $request, $response );
649             $response = $self->delete( $request, $response )
650             if $response->is_success;
651              
652             $response->code(201) unless $destexists;
653              
654             return $response;
655             }
656              
657             sub mkcol {
658             my ( $self, $request, $response ) = @_;
659             my $path = uri_unescape $request->uri->path;
660              
661             if ( !$self->_can_modify($request) ) {
662             return HTTP::Response->new( 403, 'Forbidden' );
663             }
664              
665             my $fs = $self->filesys;
666              
667             return HTTP::Response->new( 415, 'Unsupported Media Type' ) if $request->content;
668             return HTTP::Response->new( 405, 'Method Not Allowed' ) if $fs->test( 'e', $path );
669             $fs->mkdir($path);
670             if ( $fs->test( 'd', $path ) ) {
671             $response->code(201);
672             $response->message('Created');
673             }
674             else {
675             $response->code(409);
676             $response->message('Conflict');
677             }
678              
679             return $response;
680             }
681              
682             sub propfind {
683             my ( $self, $request, $response ) = @_;
684             my $path = uri_unescape $request->uri->path;
685             my $fs = $self->filesys;
686             my $depth = $request->header('Depth');
687              
688             my $reqinfo = 'allprop';
689             my @reqprops;
690             if ( $request->header('Content-Length') ) {
691             my $content = $request->content;
692             my $parser = XML::LibXML->new;
693             my $doc;
694             eval { $doc = $parser->parse_string($content); };
695             if ($@) {
696             return HTTP::Response->new( 400, 'Bad Request' );
697             }
698              
699             #$reqinfo = doc->find('/DAV:propfind/*')->localname;
700             $reqinfo = $doc->find('/*/*')->shift->localname;
701             if ( $reqinfo eq 'prop' ) {
702              
703             #for my $node ($doc->find('/DAV:propfind/DAV:prop/*')) {
704             for my $node ( $doc->find('/*/*/*')->get_nodelist ) {
705             push @reqprops, [ $node->namespaceURI, $node->localname ];
706             }
707             }
708             }
709              
710             if ( !$fs->test( 'e', $path ) ) {
711             return HTTP::Response->new( 404, 'Not Found' );
712             }
713              
714             $response->code(207);
715             $response->message('Multi-Status');
716             $response->header( 'Content-Type' => 'text/xml; charset="utf-8"' );
717              
718             my $doc = XML::LibXML::Document->new( '1.0', 'utf-8' );
719             my $multistat = $doc->createElement('D:multistatus');
720             $multistat->setAttribute( 'xmlns:D', 'DAV:' );
721             $doc->setDocumentElement($multistat);
722              
723             my @paths;
724             if ( defined $depth && $depth eq 1 and $fs->test( 'd', $path ) ) {
725             my $p = $path;
726             $p .= '/' unless $p =~ m{/$};
727             @paths = map { $p . $_ } File::Spec->no_upwards( $fs->list($path) );
728             push @paths, $path;
729             }
730             else {
731             @paths = ($path);
732             }
733              
734             for my $path (@paths) {
735             my (
736             $dev, $ino, $mode, $nlink, $uid, $gid, $rdev,
737             $size, $atime, $mtime, $ctime, $blksize, $blocks
738             ) = $fs->stat($path);
739              
740             # modified time is stringified human readable HTTP::Date style
741             $mtime = time2str($mtime);
742              
743             # created time is ISO format
744             # tidy up date format - isoz isn't exactly what we want, but
745             # it's easy to change.
746             $ctime = time2isoz($ctime);
747             $ctime =~ s/ /T/;
748             $ctime =~ s/Z//;
749              
750             $size ||= '';
751              
752             my $is_dir = $fs->test( 'd', $path );
753             my $resp = _dav_child( $multistat, 'response' );
754             my $href = File::Spec->catdir(
755             map { uri_escape $_} File::Spec->splitdir($path)
756             ) . ( $is_dir && $path !~ m{/$} ? '/' : '');
757             $href =~ tr{\\}{/}; # Protection from wrong slashes under Windows.
758             _dav_child( $resp, 'href', $href );
759             my $okprops = $doc->createElement('D:prop');
760             my $nfprops = $doc->createElement('D:prop');
761             my $prop;
762              
763             if ( $reqinfo eq 'prop' ) {
764             my %prefixes = ( 'DAV:' => 'D' );
765             my $i = 0;
766              
767             for my $reqprop (@reqprops) {
768             my ( $ns, $name ) = @$reqprop;
769             if ( $ns eq 'DAV:' && $name eq 'creationdate' ) {
770             _dav_child( $okprops, 'creationdate', $ctime );
771             }
772             elsif ( $ns eq 'DAV:' && $name eq 'getcontentlength' ) {
773             _dav_child( $okprops, 'getcontentlength', $is_dir ? () : ($size) );
774             }
775             elsif ( $ns eq 'DAV:' && $name eq 'getcontenttype' ) {
776             _dav_child( $okprops, 'getcontenttype', $is_dir ? 'httpd/unix-directory' : 'httpd/unix-file' );
777             }
778             elsif ( $ns eq 'DAV:' && $name eq 'getlastmodified' ) {
779             _dav_child( $okprops, 'getlastmodified', $mtime );
780             }
781             elsif ( $ns eq 'DAV:' && $name eq 'resourcetype' ) {
782             $prop = _dav_child( $okprops, 'resourcetype' );
783             if ( $is_dir ) {
784             _dav_child( $prop, 'collection' );
785             }
786             }
787             elsif ( $ns eq 'DAV:' && $name eq 'lockdiscovery' ) {
788             $prop = _dav_child( $okprops, 'lockdiscovery' );
789             my $user = ($request->authorization_basic())[0]||'';
790             foreach my $lock ( $self->_lock_manager()->list_all_locks({ 'path' => $path, 'user' => $user }) ) {
791             my $active = _active_lock_prop( $doc, $lock );
792             $prop->addChild( $active );
793             }
794             }
795             elsif ( $ns eq 'DAV:' && $name eq 'supportedlock' ) {
796             $prop = _supportedlock_child( $okprops );
797             }
798             else {
799             my $prefix = $prefixes{$ns};
800             if ( !defined $prefix ) {
801             $prefix = 'i' . $i++;
802              
803             # mod_dav sets 'xmlns' attribute - whatever
804             #$nfprops->setAttribute("xmlns:$prefix", $ns);
805             $resp->setAttribute( "xmlns:$prefix", $ns );
806              
807             $prefixes{$ns} = $prefix;
808             }
809              
810             $prop = $doc->createElement("$prefix:$name");
811             $nfprops->addChild($prop);
812             }
813             }
814             }
815             elsif ( $reqinfo eq 'propname' ) {
816             _dav_child( $okprops, 'creationdate' );
817             _dav_child( $okprops, 'getcontentlength' );
818             _dav_child( $okprops, 'getcontenttype' );
819             _dav_child( $okprops, 'getlastmodified' );
820             _dav_child( $okprops, 'supportedlock' );
821             _dav_child( $okprops, 'resourcetype' );
822             }
823             else {
824             _dav_child( $okprops, 'creationdate', $ctime );
825             _dav_child( $okprops, 'getcontentlength', $is_dir ? () : ($size) );
826             _dav_child( $okprops, 'getcontenttype', $is_dir ? 'httpd/unix-directory' : 'httpd/unix-file' );
827             _dav_child( $okprops, 'getlastmodified', $mtime );
828             $prop = _supportedlock_child( $okprops );
829             my $user = ($request->authorization_basic())[0]||'';
830             my @locks = $self->_lock_manager()->list_all_locks({ 'path' => $path, 'user' => $user });
831             if ( @locks ) {
832             $prop = _dav_child( $okprops, 'lockdiscovery' );
833             foreach my $lock ( @locks ) {
834             my $active = _active_lock_prop( $doc, $lock );
835             $prop->addChild( $active );
836             }
837             }
838             $prop = _dav_child( $okprops, 'resourcetype' );
839             if ( $is_dir ) {
840             _dav_child( $prop, 'collection' );
841             }
842             }
843              
844             if ( $okprops->hasChildNodes ) {
845             my $propstat = _dav_child( $resp, 'propstat' );
846             $propstat->addChild($okprops);
847             _dav_child( $propstat, 'status', 'HTTP/1.1 200 OK' );
848             }
849              
850             if ( $nfprops->hasChildNodes ) {
851             my $propstat = _dav_child( $resp, 'propstat' );
852             $propstat->addChild($nfprops);
853             _dav_child( $propstat, 'status', 'HTTP/1.1 404 Not Found' );
854             }
855             }
856              
857             #this must be 0 as certin ms webdav clients choke on 1
858             $response->content( $doc->toString(0) );
859              
860             return $response;
861             }
862              
863             sub _supportedlock_child {
864             my ($okprops) = @_;
865             my $prop = _dav_child( $okprops, 'supportedlock' );
866             #for my $n (qw(exclusive shared)) { # shared is currently not supported.
867             for my $n (qw(exclusive)) {
868             my $lock = _dav_child( $prop, 'lockentry' );
869              
870             _dav_child( _dav_child( $lock, 'lockscope' ), $n );
871             _dav_child( _dav_child( $lock, 'locktype' ), 'write' );
872             }
873              
874             return $prop;
875             }
876              
877             1;
878              
879             __END__