When using WebDAV, make sure the entire body is in before we start sending
[pr0n] / perl / Sesse / pr0n / WebDAV.pm
1 package Sesse::pr0n::WebDAV;
2 use strict;
3 use warnings;
4
5 use Sesse::pr0n::Common qw(error dberror);
6 use Digest::SHA1;
7 use MIME::Base64;
8
9 sub handler {
10         my $r = shift;
11         my $dbh = Sesse::pr0n::Common::get_dbh();
12         
13         # We ignore the body, but we _must_ consume it fully before
14         # we output anything, or Squid will get seriously confused
15         $r->discard_request_body;
16
17         $r->headers_out->{'DAV'} = "1,2";
18
19         # We only handle depth=0, depth=1 (cf. the RFC)
20         my $depth = $r->headers_in->{'depth'};
21         $depth = 0 if (!defined($depth));
22         if (defined($depth) && $depth ne "0" && $depth ne "1") {
23                 $r->content_type('text/plain; charset="utf-8"');
24                 $r->status(403);
25                 $r->print("Invalid depth setting");
26                 return Apache2::Const::OK;
27         }
28
29         my ($user,$takenby) = Sesse::pr0n::Common::check_access($r);
30         if (!defined($user)) {
31                 return Apache2::Const::OK;
32         }
33
34         # Just "ping, are you alive and do you speak WebDAV"
35         if ($r->method eq "OPTIONS") {
36                 $r->content_type('text/plain; charset="utf-8"');
37                 $r->status(200);
38                 $r->headers_out->{'allow'} = 'OPTIONS,PUT';
39                 $r->headers_out->{'ms-author-via'} = 'DAV';
40                 return Apache2::Const::OK;
41         }
42         
43         # Directory listings et al
44         if ($r->method eq "PROPFIND") {
45                 $r->content_type('text/xml; charset="utf-8"');
46                 $r->status(207);
47
48                 if ($r->uri =~ m#^/webdav/?$#) {
49                         $r->headers_out->{'content-location'} = "/webdav/";
50                 
51                         # Root directory
52                         $r->print(<<"EOF");
53 <?xml version="1.0" encoding="utf-8"?>
54 <multistatus xmlns="DAV:">
55   <response>
56      <href>/webdav/</href>
57      <propstat>
58         <prop>
59           <resourcetype><collection/></resourcetype>
60           <getcontenttype>text/xml</getcontenttype>
61         </prop>
62         <status>HTTP/1.1 200 OK</status>
63      </propstat>
64   </response>
65 EOF
66
67                         # Optionally list the upload/ dir
68                         if ($depth >= 1) {
69                                 $r->print(<<"EOF");
70   <response>
71      <href>/webdav/upload/</href>
72      <propstat>
73         <prop>
74           <resourcetype><collection/></resourcetype>
75           <getcontenttype>text/xml</getcontenttype>
76         </prop>
77         <status>HTTP/1.1 200 OK</status>
78      </propstat>
79   </response>
80 EOF
81                         }
82                         $r->print("</multistatus>\n");
83                  } elsif ($r->uri =~ m#^/webdav/upload/?$#) {
84                         $r->headers_out->{'content-location'} = "/webdav/upload/";
85                         
86                         # Upload root directory
87                         $r->print(<<"EOF");
88 <?xml version="1.0" encoding="utf-8"?>
89 <multistatus xmlns="DAV:">
90   <response>
91      <href>/webdav/upload/</href>
92      <propstat>
93         <prop>
94           <resourcetype><collection/></resourcetype>
95           <getcontenttype>text/xml</getcontenttype>
96         </prop>
97         <status>HTTP/1.1 200 OK</status>
98      </propstat>
99   </response>
100 EOF
101
102                         # Optionally list all events
103                         if ($depth >= 1) {
104                                 my $q = $dbh->prepare('SELECT * FROM events WHERE vhost=?') or
105                                         dberror($r, "Couldn't list events");
106                                 $q->execute($r->get_server_name) or
107                                         dberror($r, "Couldn't get events");
108                 
109                                 while (my $ref = $q->fetchrow_hashref()) {
110                                         my $id = $ref->{'id'};
111                                         my $name = $ref->{'name'};
112                                 
113                                         $name =~ s/&/\&amp;/g;  # hack :-)
114                                         $r->print(<<"EOF");
115   <response>
116      <href>/webdav/upload/$id/</href>
117      <propstat>
118         <prop>
119           <resourcetype><collection/></resourcetype>
120           <getcontenttype>text/xml</getcontenttype>
121           <displayname>$name</displayname> 
122         </prop>
123         <status>HTTP/1.1 200 OK</status>
124      </propstat>
125   </response>
126 EOF
127                                 }
128                                 $q->finish;
129                         }
130
131                         $r->print("</multistatus>\n");
132                 } elsif ($r->uri =~ m#^/webdav/upload/([a-zA-Z0-9-]+)/?$#) {
133                         my $event = $1;
134                         
135                         $r->headers_out->{'content-location'} = "/webdav/upload/$event/";
136                         
137                         # Check that we do indeed exist
138                         my $ref = $dbh->selectrow_hashref('SELECT count(*) AS numev FROM events WHERE id=?',
139                                 undef, $event);
140                         if ($ref->{'numev'} != 1) {
141                                 $r->status(404);
142                                 $r->content_type('text/plain; charset=utf-8');
143                                 $r->print("Couldn't find event in database");
144                                 return Apache2::Const::OK;
145                         }
146                         
147                         # OK, list the directory
148                         $r->print(<<"EOF");
149 <?xml version="1.0" encoding="utf-8"?>
150 <multistatus xmlns="DAV:">
151   <response>
152      <href>/webdav/upload/$event/</href>
153      <propstat>
154         <prop>
155           <resourcetype><collection/></resourcetype>
156           <getcontenttype>text/xml</getcontenttype>
157         </prop>
158         <status>HTTP/1.1 200 OK</status>
159      </propstat>
160   </response>
161 EOF
162
163                         # List all the files within too, of course :-)
164                         if ($depth >= 1) {
165                                 my $q = $dbh->prepare('SELECT * FROM images WHERE event=?') or
166                                         dberror($r, "Couldn't list images");
167                                 $q->execute($event) or
168                                         dberror($r, "Couldn't get events");
169                 
170                                 while (my $ref = $q->fetchrow_hashref()) {
171                                         my $id = $ref->{'id'};
172                                         my $filename = $ref->{'filename'};
173                                         my $fname = Sesse::pr0n::Common::get_disk_location($r, $id);
174                                         my (undef, undef, undef, undef, undef, undef, undef, $size, undef, $mtime) = stat($fname)
175                                                 or next;
176                                         $mtime = POSIX::strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime($mtime));
177                                         my $mime_type = Sesse::pr0n::Common::get_mimetype_from_filename($filename);
178
179                                         $r->print(<<"EOF");
180   <response>
181      <href>/webdav/upload/$event/$filename</href>
182      <propstat>
183         <prop>
184           <resourcetype/>
185           <getcontenttype>$mime_type</getcontenttype>
186           <getcontentlength>$size</getcontentlength>
187           <getlastmodified>$mtime</getlastmodified>
188         </prop>
189         <status>HTTP/1.1 200 OK</status>
190      </propstat>
191   </response>
192 EOF
193                                 }
194                                 $q->finish;
195
196                                 # And the magical autorename folder
197                                 $r->print(<<"EOF");
198   <response>
199      <href>/webdav/upload/$event/autorename/</href>
200      <propstat>
201         <prop>
202           <resourcetype><collection/></resourcetype>
203           <getcontenttype>text/xml</getcontenttype>
204         </prop>
205         <status>HTTP/1.1 200 OK</status>
206      </propstat>
207   </response>
208 EOF
209                                 $r->log->info("Full list");
210                         }
211
212                         $r->print("</multistatus>\n");
213
214                         return Apache2::Const::OK;
215                 } elsif ($r->uri =~ m#^/webdav/upload/([a-zA-Z0-9-]+)/autorename/?$#) {
216                         # The autorename folder is always empty
217                         my $event = $1;
218                         
219                         $r->headers_out->{'content-location'} = "/webdav/upload/$event/autorename/";
220                         
221                         # Check that we do indeed exist
222                         my $ref = $dbh->selectrow_hashref('SELECT count(*) AS numev FROM events WHERE id=?',
223                                 undef, $event);
224                         if ($ref->{'numev'} != 1) {
225                                 $r->status(404);
226                                 $r->content_type('text/plain; charset=utf-8');
227                                 $r->print("Couldn't find event in database");
228                                 return Apache2::Const::OK;
229                         }
230                         
231                         # OK, list the (empty) directory
232                         $r->print(<<"EOF");
233 <?xml version="1.0" encoding="utf-8"?>
234 <multistatus xmlns="DAV:">
235   <response>
236      <href>/webdav/upload/$event/autorename/</href>
237      <propstat>
238         <prop>
239           <resourcetype><collection/></resourcetype>
240           <getcontenttype>text/xml</getcontenttype>
241         </prop>
242         <status>HTTP/1.1 200 OK</status>
243      </propstat>
244   </response>
245 </multistatus>
246 EOF
247         
248                         return Apache2::Const::OK;
249                 } elsif ($r->uri =~ m#^/webdav/upload/([a-zA-Z0-9-]+)/([a-zA-Z0-9._-]+)$#) {
250                         # stat a single file
251                         my ($event, $filename) = ($1, $2);
252                         my ($fname, $size, $mtime);
253                         
254                         # check if we have a pending fake file for this
255                         my $ref = $dbh->selectrow_hashref('SELECT count(*) AS numfiles FROM fake_files WHERE event=? AND filename=? AND expires_at > now()',
256                                 undef, $event, $filename);
257                         if ($ref->{'numfiles'} == 1) {
258                                 $fname = "/dev/null";
259                                 $size = 0;
260                                 $mtime = time;
261                         } else {
262                                 ($fname, $size, $mtime) = Sesse::pr0n::Common::stat_image($r, $event, $filename);
263                         }
264                         
265                         if (!defined($fname)) {
266                                 $r->status(404);
267                                 $r->content_type('text/plain; charset=utf-8');
268                                 $r->print("Couldn't find file");
269                                 return Apache2::Const::OK;
270                         }
271                         my $mime_type = Sesse::pr0n::Common::get_mimetype_from_filename($filename);
272                         
273                         $mtime = POSIX::strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime($mtime));
274                         $r->print(<<"EOF");
275 <?xml version="1.0" encoding="utf-8"?>
276 <multistatus xmlns="DAV:">
277   <response>
278     <href>/webdav/upload/$event/$filename</href>
279     <propstat>
280       <prop>
281         <resourcetype/>
282         <getcontenttype>$mime_type</getcontenttype>
283         <getcontentlength>$size</getcontentlength>
284         <getlastmodified>$mtime</getlastmodified>
285       </prop>
286       <status>HTTP/1.1 200 OK</status>
287     </propstat>
288   </response>
289 </multistatus>
290 EOF
291                         return Apache2::Const::OK;
292                 } elsif ($r->uri =~ m#^/webdav/upload/([a-zA-Z0-9-]+)/autorename/(.{1,250})$#) {
293                         # stat a single file in autorename
294                         my ($event, $filename) = ($1, $2);
295                         my ($fname, $size, $mtime);
296                         
297                         # check if we have a pending fake file for this
298                         my $ref = $dbh->selectrow_hashref('SELECT count(*) AS numfiles FROM fake_files WHERE event=? AND filename=? AND expires_at > now()',
299                                 undef, $event, $filename);
300                         if ($ref->{'numfiles'} == 1) {
301                                 $fname = "/dev/null";
302                                 $size = 0;
303                                 $mtime = time;
304                         } else {
305                                 # check if we have a "shadow file" for this
306                                 my $ref = $dbh->selectrow_hashref('SELECT id FROM shadow_files WHERE event=? AND filename=? AND expires_at > now()',
307                                         undef, $event, $filename);
308                                 if (defined($ref)) {
309                                         ($fname, $size, $mtime) = Sesse::pr0n::Common::stat_image_from_id($r, $ref->{'id'});
310                                 }
311                         }
312                         
313                         if (!defined($fname)) {
314                                 $r->status(404);
315                                 $r->content_type('text/plain; charset=utf-8');
316                                 $r->print("Couldn't find file");
317                                 return Apache2::Const::OK;
318                         }
319                         my $mime_type = Sesse::pr0n::Common::get_mimetype_from_filename($filename);
320                         
321                         $mtime = POSIX::strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime($mtime));
322                         $r->print(<<"EOF");
323 <?xml version="1.0" encoding="utf-8"?>
324 <multistatus xmlns="DAV:">
325   <response>
326     <href>/webdav/upload/$event/autorename/$filename</href>
327     <propstat>
328       <prop>
329         <resourcetype/>
330         <getcontenttype>$mime_type</getcontenttype>
331         <getcontentlength>$size</getcontentlength>
332         <getlastmodified>$mtime</getlastmodified>
333       </prop>
334       <status>HTTP/1.1 200 OK</status>
335     </propstat>
336   </response>
337 </multistatus>
338 EOF
339                 } else {
340                         $r->status(404);
341                         $r->content_type('text/plain; charset=utf-8');
342                         $r->print("Couldn't find file");
343                 }
344                 return Apache2::Const::OK;
345         }
346         
347         if ($r->method eq "HEAD" or $r->method eq "GET") {
348                 if ($r->uri !~ m#^/webdav/upload/([a-zA-Z0-9-]+)/(autorename/)?(.{1,250})$#) {
349                         $r->status(404);
350                         $r->content_type('text/xml; charset=utf-8');
351                         $r->print("<?xml version=\"1.0\"?>\n<p>Couldn't find file</p>");
352                         return Apache2::Const::OK;
353                 }
354
355                 my ($event, $autorename, $filename) = ($1, $2, $3);
356                 
357                 # Check if this file really exists
358                 my ($fname, $size, $mtime);
359
360                 # check if we have a pending fake file for this
361                 my $ref = $dbh->selectrow_hashref('SELECT count(*) AS numfiles FROM fake_files WHERE event=? AND filename=? AND expires_at > now()',
362                         undef, $event, $filename);
363                 if ($ref->{'numfiles'} == 1) {
364                         $fname = "/dev/null";
365                         $size = 0;
366                         $mtime = time;
367                 } else {
368                         # check if we have a "shadow file" for this
369                         if (defined($autorename) && $autorename eq "autorename/") {
370                                 my $ref = $dbh->selectrow_hashref('SELECT id FROM shadow_files WHERE event=? AND filename=? AND expires_at > now()',
371                                         undef, $event, $filename);
372                                 if (defined($ref)) {
373                                         ($fname, $size, $mtime) = Sesse::pr0n::Common::stat_image_from_id($r, $ref->{'id'});
374                                 }
375                         } elsif (!defined($fname)) {
376                                 ($fname, $size, $mtime) = Sesse::pr0n::Common::stat_image($r, $event, $filename);
377                         }
378                 }
379                 
380                 if (!defined($fname)) {
381                         $r->status(404);
382                         $r->content_type('text/plain; charset=utf-8');
383                         $r->print("Couldn't find file");
384                         return Apache2::Const::OK;
385                 }
386                 
387                 $r->status(200);
388                 $r->set_content_length($size);
389                 $r->set_last_modified($mtime);
390         
391                 if ($r->method eq "GET") {
392                         $r->sendfile($fname);
393                 }
394                 return Apache2::Const::OK;
395         }
396         
397         if ($r->method eq "PUT") {
398                 if ($r->uri !~ m#^/webdav/upload/([a-zA-Z0-9-]+)/(autorename/)?(.{1,250})$#) {
399                         $r->status(403);
400                         $r->content_type('text/plain; charset=utf-8');
401                         $r->print("No access");
402                         return Apache2::Const::OK;
403                 }
404                 
405                 my ($event, $autorename, $filename) = ($1, $2, $3);
406                 my $size = $r->headers_in->{'content-length'};
407                 my $orig_filename = $filename;
408
409                 # Remove evil characters
410                 if ($filename =~ /[^a-zA-Z0-9._-]/) {
411                         if (defined($autorename) && $autorename eq "autorename/") {
412                                 $filename =~ tr/a-zA-Z0-9.-/_/c;
413                         } else {
414                                 $r->status(403);
415                                 $r->content_type('text/plain; charset=utf-8');
416                                 $r->print("Illegal characters in filename");
417                                 return Apache2::Const::OK;
418                         }
419                 }
420                 
421                 #
422                 # gnome-vfs and mac os x love to make zero-byte files,
423                 # make them happy
424                 # 
425                 if ($r->headers_in->{'content-length'} == 0) {
426                         $dbh->do('DELETE FROM fake_files WHERE expires_at <= now() OR (event=? AND filename=?);',
427                                 undef, $event, $filename)
428                                 or dberror($r, "Couldn't prune fake_files");
429                         $dbh->do('INSERT INTO fake_files (event,filename,expires_at) VALUES (?,?,now() + interval \'30 seconds\');',
430                                 undef, $event, $filename)
431                                 or dberror($r, "Couldn't add file");
432                         $r->content_type('text/plain; charset="utf-8"');
433                         $r->status(201);
434                         $r->print("OK");
435                         $r->log->info("Fake upload of $event/$filename");
436                         return Apache2::Const::OK;
437                 }
438
439                 # Get the new ID
440                 my $ref = $dbh->selectrow_hashref("SELECT NEXTVAL('imageid_seq') AS id;");
441                 my $newid = $ref->{'id'};
442                 if (!defined($newid)) {
443                         dberror($r, "Couldn't get new ID");
444                 }
445                 
446                 # Autorename if we need to
447                 if (defined($autorename) && $autorename eq "autorename/") {
448                         my $ref = $dbh->selectrow_hashref("SELECT COUNT(*) AS numfiles FROM images WHERE event=? AND filename=?",
449                                 undef, $event, $filename)
450                                 or dberror($r, "Couldn't check for existing files");
451                         if ($ref->{'numfiles'} > 0) {
452                                 $r->log->info("Renaming $filename to $newid.jpeg");
453                                 $filename = "$newid.jpeg";
454                         }
455                 }
456                 
457                 {
458                         # Enable transactions and error raising temporarily
459                         local $dbh->{AutoCommit} = 0;
460                         
461                         local $dbh->{RaiseError} = 1;
462
463                         # Try to insert this new file
464                         eval {
465                                 $dbh->do('DELETE FROM fake_files WHERE event=? AND filename=?;',
466                                         undef, $event, $filename);
467                                         
468                                 $dbh->do('INSERT INTO images (id,event,uploadedby,takenby,filename) VALUES (?,?,?,?,?);',
469                                         undef, $newid, $event, $user, $takenby, $filename);
470
471                                 # Now save the file to disk
472                                 my $fname = Sesse::pr0n::Common::get_disk_location($r, $newid);
473                                 open NEWFILE, ">$fname"
474                                         or die "$fname: $!";
475
476                                 my $buf;
477                                 my $content_length = $r->headers_in->{'content-length'};
478                                 if ($r->read($buf, $content_length)) {
479                                         print NEWFILE $buf or die "write($fname): $!";
480                                 }
481
482                                 close NEWFILE or die "close($fname): $!";
483                                 
484                                 # Orient stuff correctly
485                                 system("/usr/bin/exifautotran", $fname) == 0
486                                         or die "/usr/bin/exifautotran: $!";
487
488                                 # Make cache while we're at it.
489                                 # Don't do it for the resource forks Mac OS X loves to upload :-(
490                                 if ($filename !~ /^\._/) {
491                                         Sesse::pr0n::Common::ensure_cached($r, $filename, $newid, -1, -1, 1, 80, 64, 320, 256, -1, -1);
492                                 }
493                                 
494                                 # OK, we got this far, commit
495                                 $dbh->commit;
496
497                                 $r->log->notice("Successfully wrote $event/$filename to $fname");
498                         };
499                         if ($@) {
500                                 # Some error occurred, rollback and bomb out
501                                 $dbh->rollback;
502                                 dberror($r, "Transaction aborted because $@");
503                         }
504                 }
505
506                 # Insert a `shadow file' we can stat the next 30 secs
507                 if (defined($autorename) && $autorename eq "autorename/") {
508                         $dbh->do('DELETE FROM shadow_files WHERE expires_at <= now() OR (event=? AND filename=?);',
509                                 undef, $event, $filename)
510                                 or dberror($r, "Couldn't prune shadow_files");
511                         $dbh->do('INSERT INTO shadow_files (event,filename,id,expires_at) VALUES (?,?,?,now() + interval \'30 seconds\');',
512                                 undef, $event, $orig_filename, $newid)
513                                 or dberror($r, "Couldn't add shadow file");
514                         $r->log->info("Added shadow entry for $event/$filename");
515                 }
516
517                 $r->content_type('text/plain; charset="utf-8"');
518                 $r->status(201);
519                 $r->print("OK");
520
521                 return Apache2::Const::OK;
522         }
523
524         # Yes, we fake locks. :-)
525         if ($r->method eq "LOCK") {
526                 if ($r->uri !~ m#^/webdav/upload/([a-zA-Z0-9-]+)/(autorename/)?([a-zA-Z0-9._-]+)$#) {
527                         $r->status(403);
528                         $r->content_type('text/plain; charset=utf-8');
529                         $r->print("No access");
530                         return Apache2::Const::OK;
531                 }
532
533                 my ($event, $autorename, $filename) = ($1, $2, $3);
534                 my $sha1 = Digest::SHA1::sha1_base64("/$event/$autorename/$filename");
535
536                 $r->status(200);
537                 $r->content_type('text/xml; charset=utf-8');
538
539                 $r->print(<<"EOF");
540 <?xml version="1.0" encoding="utf-8"?>
541 <prop xmlns="DAV:">
542   <lockdiscovery>
543     <activelock>
544       <locktype><write/></locktype>
545       <lockscope><exclusive/></lockscope>
546       <depth>0</depth>
547       <owner>
548         <href>/webdav/upload/$event/$autorename$filename</href>
549       </owner>
550       <timeout>Second-3600</timeout>
551       <locktoken>
552         <href>opaquelocktoken:$sha1</href>
553       </locktoken>
554     </activelock>
555   </lockdiscovery>
556 </prop>
557 EOF
558                 return Apache2::Const::OK;
559         }
560         
561         if ($r->method eq "UNLOCK") {
562                 $r->content_type('text/plain; charset="utf-8"');
563                 $r->status(200);
564                 $r->print("OK");
565
566                 return Apache2::Const::OK;
567         }
568
569         if ($r->method eq "DELETE") {
570                 if ($r->uri !~ m#^/webdav/upload/([a-zA-Z0-9-]+)/(autorename/)?(\._[a-zA-Z0-9._-]+)$#) {
571                         $r->status(403);
572                         $r->content_type('text/plain; charset=utf-8');
573                         $r->print("No access");
574                         return Apache2::Const::OK;
575                 }
576                 
577                 my ($event, $autorename, $filename) = ($1, $2, $3);
578                 $dbh->do('DELETE FROM images WHERE event=? AND filename=?;',
579                         undef, $event, $filename)
580                         or dberror($r, "Couldn't remove file");
581                 $r->status(200);
582                 $r->print("OK");
583
584                 $r->log->info("deleted $event/$filename");
585                 
586                 return Apache2::Const::OK;
587         }
588         
589         if ($r->method eq "MOVE" or
590             $r->method eq "MKCOL" or
591             $r->method eq "RMCOL" or
592             $r->method eq "RENAME" or
593             $r->method eq "COPY") {
594                 $r->content_type('text/plain; charset="utf-8"');
595                 $r->status(403);
596                 $r->print("Sorry, you do not have access to that feature.");
597                 return Apache2::Const::OK;
598         }
599         
600         $r->content_type('text/plain; charset=utf-8');
601         $r->log->error("unknown method " . $r->method);
602         $r->status(500);
603         $r->print("Unknown method");
604         
605         return Apache2::Const::OK;
606 }
607
608 1;
609
610