]> git.sesse.net Git - pr0n/blob - perl/Sesse/pr0n/WebDAV.pm
Unlink all failed uploads.
[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         $r->headers_out->{'DAV'} = "1,2";
14
15         # We only handle depth=0, depth=1 (cf. the RFC)
16         my $depth = $r->headers_in->{'depth'};
17         $depth = 0 if (!defined($depth));
18         if (defined($depth) && $depth ne "0" && $depth ne "1") {
19                 $r->content_type('text/plain; charset="utf-8"');
20                 $r->status(403);
21                 $r->print("Invalid depth setting");
22                 return Apache2::Const::OK;
23         }
24
25         my ($user,$takenby) = Sesse::pr0n::Common::check_access($r);
26         if (!defined($user)) {
27                 return Apache2::Const::OK;
28         }
29
30         # Just "ping, are you alive and do you speak WebDAV"
31         if ($r->method eq "OPTIONS") {
32                 $r->content_type('text/plain; charset="utf-8"');
33                 $r->status(200);
34                 $r->headers_out->{'allow'} = 'OPTIONS,PUT';
35                 $r->headers_out->{'ms-author-via'} = 'DAV';
36                 return Apache2::Const::OK;
37         }
38         
39         # Directory listings et al
40         if ($r->method eq "PROPFIND") {
41                 # We ignore the body, but we _must_ consume it fully before
42                 # we output anything, or Squid will get seriously confused
43                 $r->discard_request_body;
44
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                         }
210
211                         $r->print("</multistatus>\n");
212
213                         return Apache2::Const::OK;
214                 } elsif ($r->uri =~ m#^/webdav/upload/([a-zA-Z0-9-]+)/autorename/?$#) {
215                         # The autorename folder is always empty
216                         my $event = $1;
217                         
218                         $r->headers_out->{'content-location'} = "/webdav/upload/$event/autorename/";
219                         
220                         # Check that we do indeed exist
221                         my $ref = $dbh->selectrow_hashref('SELECT count(*) AS numev FROM events WHERE id=?',
222                                 undef, $event);
223                         if ($ref->{'numev'} != 1) {
224                                 $r->status(404);
225                                 $r->content_type('text/plain; charset=utf-8');
226                                 $r->print("Couldn't find event in database");
227                                 return Apache2::Const::OK;
228                         }
229                         
230                         # OK, list the (empty) directory
231                         $r->print(<<"EOF");
232 <?xml version="1.0" encoding="utf-8"?>
233 <multistatus xmlns="DAV:">
234   <response>
235      <href>/webdav/upload/$event/autorename/</href>
236      <propstat>
237         <prop>
238           <resourcetype><collection/></resourcetype>
239           <getcontenttype>text/xml</getcontenttype>
240         </prop>
241         <status>HTTP/1.1 200 OK</status>
242      </propstat>
243   </response>
244 </multistatus>
245 EOF
246         
247                         return Apache2::Const::OK;
248                 } elsif ($r->uri =~ m#^/webdav/upload/([a-zA-Z0-9-]+)/([a-zA-Z0-9._-]+)$#) {
249                         # stat a single file
250                         my ($event, $filename) = ($1, $2);
251                         my ($fname, $size, $mtime);
252                         
253                         # check if we have a pending fake file for this
254                         my $ref = $dbh->selectrow_hashref('SELECT count(*) AS numfiles FROM fake_files WHERE event=? AND filename=? AND expires_at > now()',
255                                 undef, $event, $filename);
256                         if ($ref->{'numfiles'} == 1) {
257                                 $fname = "/dev/null";
258                                 $size = 0;
259                                 $mtime = time;
260                         } else {
261                                 ($fname, $size, $mtime) = Sesse::pr0n::Common::stat_image($r, $event, $filename);
262                         }
263                         
264                         if (!defined($fname)) {
265                                 $r->status(404);
266                                 $r->content_type('text/plain; charset=utf-8');
267                                 $r->print("Couldn't find file");
268                                 return Apache2::Const::OK;
269                         }
270                         my $mime_type = Sesse::pr0n::Common::get_mimetype_from_filename($filename);
271                         
272                         $mtime = POSIX::strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime($mtime));
273                         $r->print(<<"EOF");
274 <?xml version="1.0" encoding="utf-8"?>
275 <multistatus xmlns="DAV:">
276   <response>
277     <href>/webdav/upload/$event/$filename</href>
278     <propstat>
279       <prop>
280         <resourcetype/>
281         <getcontenttype>$mime_type</getcontenttype>
282         <getcontentlength>$size</getcontentlength>
283         <getlastmodified>$mtime</getlastmodified>
284       </prop>
285       <status>HTTP/1.1 200 OK</status>
286     </propstat>
287   </response>
288 </multistatus>
289 EOF
290                         return Apache2::Const::OK;
291                 } elsif ($r->uri =~ m#^/webdav/upload/([a-zA-Z0-9-]+)/autorename/(.{1,250})$#) {
292                         # stat a single file in autorename
293                         my ($event, $filename) = ($1, $2);
294                         my ($fname, $size, $mtime);
295                         
296                         # check if we have a pending fake file for this
297                         my $ref = $dbh->selectrow_hashref('SELECT count(*) AS numfiles FROM fake_files WHERE event=? AND filename=? AND expires_at > now()',
298                                 undef, $event, $filename);
299                         if ($ref->{'numfiles'} == 1) {
300                                 $fname = "/dev/null";
301                                 $size = 0;
302                                 $mtime = time;
303                         } else {
304                                 # check if we have a "shadow file" for this
305                                 my $ref = $dbh->selectrow_hashref('SELECT id FROM shadow_files WHERE event=? AND filename=? AND expires_at > now()',
306                                         undef, $event, $filename);
307                                 if (defined($ref)) {
308                                         ($fname, $size, $mtime) = Sesse::pr0n::Common::stat_image_from_id($r, $ref->{'id'});
309                                 }
310                         }
311                         
312                         if (!defined($fname)) {
313                                 $r->status(404);
314                                 $r->content_type('text/plain; charset=utf-8');
315                                 $r->print("Couldn't find file");
316                                 return Apache2::Const::OK;
317                         }
318                         my $mime_type = Sesse::pr0n::Common::get_mimetype_from_filename($filename);
319                         
320                         $mtime = POSIX::strftime("%a, %d %b %Y %H:%M:%S GMT", gmtime($mtime));
321                         $r->print(<<"EOF");
322 <?xml version="1.0" encoding="utf-8"?>
323 <multistatus xmlns="DAV:">
324   <response>
325     <href>/webdav/upload/$event/autorename/$filename</href>
326     <propstat>
327       <prop>
328         <resourcetype/>
329         <getcontenttype>$mime_type</getcontenttype>
330         <getcontentlength>$size</getcontentlength>
331         <getlastmodified>$mtime</getlastmodified>
332       </prop>
333       <status>HTTP/1.1 200 OK</status>
334     </propstat>
335   </response>
336 </multistatus>
337 EOF
338                 } else {
339                         $r->status(404);
340                         $r->content_type('text/plain; charset=utf-8');
341                         $r->print("Couldn't find file");
342                 }
343                 return Apache2::Const::OK;
344         }
345         
346         if ($r->method eq "HEAD" or $r->method eq "GET") {
347                 if ($r->uri !~ m#^/webdav/upload/([a-zA-Z0-9-]+)/(autorename/)?(.{1,250})$#) {
348                         $r->status(404);
349                         $r->content_type('text/xml; charset=utf-8');
350                         $r->print("<?xml version=\"1.0\"?>\n<p>Couldn't find file</p>");
351                         return Apache2::Const::OK;
352                 }
353
354                 my ($event, $autorename, $filename) = ($1, $2, $3);
355                 
356                 # Check if this file really exists
357                 my ($fname, $size, $mtime);
358
359                 # check if we have a pending fake file for this
360                 my $ref = $dbh->selectrow_hashref('SELECT count(*) AS numfiles FROM fake_files WHERE event=? AND filename=? AND expires_at > now()',
361                         undef, $event, $filename);
362                 if ($ref->{'numfiles'} == 1) {
363                         $fname = "/dev/null";
364                         $size = 0;
365                         $mtime = time;
366                 } else {
367                         # check if we have a "shadow file" for this
368                         if (defined($autorename) && $autorename eq "autorename/") {
369                                 my $ref = $dbh->selectrow_hashref('SELECT id FROM shadow_files WHERE event=? AND filename=? AND expires_at > now()',
370                                         undef, $event, $filename);
371                                 if (defined($ref)) {
372                                         ($fname, $size, $mtime) = Sesse::pr0n::Common::stat_image_from_id($r, $ref->{'id'});
373                                 }
374                         } elsif (!defined($fname)) {
375                                 ($fname, $size, $mtime) = Sesse::pr0n::Common::stat_image($r, $event, $filename);
376                         }
377                 }
378                 
379                 if (!defined($fname)) {
380                         $r->status(404);
381                         $r->content_type('text/plain; charset=utf-8');
382                         $r->print("Couldn't find file");
383                         return Apache2::Const::OK;
384                 }
385                 
386                 $r->status(200);
387                 $r->set_content_length($size);
388                 $r->set_last_modified($mtime);
389         
390                 if ($r->method eq "GET") {
391                         $r->sendfile($fname);
392                 }
393                 return Apache2::Const::OK;
394         }
395         
396         if ($r->method eq "PUT") {
397                 if ($r->uri !~ m#^/webdav/upload/([a-zA-Z0-9-]+)/(autorename/)?(.{1,250})$#) {
398                         $r->status(403);
399                         $r->content_type('text/plain; charset=utf-8');
400                         $r->print("No access");
401                         return Apache2::Const::OK;
402                 }
403                 
404                 my ($event, $autorename, $filename) = ($1, $2, $3);
405                 my $size = $r->headers_in->{'content-length'};
406                 my $orig_filename = $filename;
407
408                 # Remove evil characters
409                 if ($filename =~ /[^a-zA-Z0-9._-]/) {
410                         if (defined($autorename) && $autorename eq "autorename/") {
411                                 $filename =~ tr/a-zA-Z0-9.-/_/c;
412                         } else {
413                                 $r->status(403);
414                                 $r->content_type('text/plain; charset=utf-8');
415                                 $r->print("Illegal characters in filename");
416                                 return Apache2::Const::OK;
417                         }
418                 }
419                 
420                 #
421                 # gnome-vfs and mac os x love to make zero-byte files,
422                 # make them happy
423                 # 
424                 if ($r->headers_in->{'content-length'} == 0) {
425                         $dbh->do('DELETE FROM fake_files WHERE expires_at <= now() OR (event=? AND filename=?);',
426                                 undef, $event, $filename)
427                                 or dberror($r, "Couldn't prune fake_files");
428                         $dbh->do('INSERT INTO fake_files (event,filename,expires_at) VALUES (?,?,now() + interval \'30 seconds\');',
429                                 undef, $event, $filename)
430                                 or dberror($r, "Couldn't add file");
431                         $r->content_type('text/plain; charset="utf-8"');
432                         $r->status(201);
433                         $r->print("OK");
434                         $r->log->info("Fake upload of $event/$filename");
435                         return Apache2::Const::OK;
436                 }
437
438                 # Get the new ID
439                 my $ref = $dbh->selectrow_hashref("SELECT NEXTVAL('imageid_seq') AS id;");
440                 my $newid = $ref->{'id'};
441                 if (!defined($newid)) {
442                         dberror($r, "Couldn't get new ID");
443                 }
444                 
445                 # Autorename if we need to
446                 if (defined($autorename) && $autorename eq "autorename/") {
447                         my $ref = $dbh->selectrow_hashref("SELECT COUNT(*) AS numfiles FROM images WHERE event=? AND filename=?",
448                                 undef, $event, $filename)
449                                 or dberror($r, "Couldn't check for existing files");
450                         if ($ref->{'numfiles'} > 0) {
451                                 $r->log->info("Renaming $filename to $newid.jpeg");
452                                 $filename = "$newid.jpeg";
453                         }
454                 }
455                 
456                 {
457                         # Enable transactions and error raising temporarily
458                         local $dbh->{AutoCommit} = 0;
459                         local $dbh->{RaiseError} = 1;
460                         my $fname;
461
462                         # Try to insert this new file
463                         eval {
464                                 $dbh->do('DELETE FROM fake_files WHERE event=? AND filename=?;',
465                                         undef, $event, $filename);
466                                         
467                                 $dbh->do('INSERT INTO images (id,event,uploadedby,takenby,filename) VALUES (?,?,?,?,?);',
468                                         undef, $newid, $event, $user, $takenby, $filename);
469
470                                 # Now save the file to disk
471                                 $fname = Sesse::pr0n::Common::get_disk_location($r, $newid);
472                                 open NEWFILE, ">$fname"
473                                         or die "$fname: $!";
474
475                                 my $buf;
476                                 my $content_length = $r->headers_in->{'content-length'};
477                                 if ($r->read($buf, $content_length)) {
478                                         print NEWFILE $buf or die "write($fname): $!";
479                                 }
480
481                                 close NEWFILE or die "close($fname): $!";
482                                 
483                                 # Orient stuff correctly
484                                 system("/usr/bin/exifautotran", $fname) == 0
485                                         or die "/usr/bin/exifautotran: $!";
486
487                                 # Make cache while we're at it.
488                                 # Don't do it for the resource forks Mac OS X loves to upload :-(
489                                 if ($filename !~ /^\._/) {
490                                         Sesse::pr0n::Common::ensure_cached($r, $filename, $newid, -1, -1, 1, 80, 64, 320, 256, -1, -1);
491                                 }
492                                 
493                                 # OK, we got this far, commit
494                                 $dbh->commit;
495
496                                 $r->log->notice("Successfully wrote $event/$filename to $fname");
497                         };
498                         if ($@) {
499                                 # Some error occurred, rollback and bomb out
500                                 $dbh->rollback;
501                                 dberror($r, "Transaction aborted because $@");
502                                 unlink($fname);
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