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, |
||||||
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 | 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 |
||||||
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__ |