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