9d23bb9668af0860e186f0a25ce3a2d1cb7dd559
[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                                 $dbh->do('UPDATE events SET last_update=CURRENT_TIMESTAMP WHERE event=?',
470                                         undef, $event);
471
472                                 # Now save the file to disk
473                                 $fname = Sesse::pr0n::Common::get_disk_location($r, $newid);
474                                 open NEWFILE, ">$fname"
475                                         or die "$fname: $!";
476
477                                 my $buf;
478                                 my $content_length = $r->headers_in->{'content-length'};
479                                 if ($r->read($buf, $content_length)) {
480                                         print NEWFILE $buf or die "write($fname): $!";
481                                 }
482
483                                 close NEWFILE or die "close($fname): $!";
484                                 
485                                 # Orient stuff correctly
486                                 system("/usr/bin/exifautotran", $fname) == 0
487                                         or die "/usr/bin/exifautotran: $!";
488
489                                 # Make cache while we're at it.
490                                 # Don't do it for the resource forks Mac OS X loves to upload :-(
491                                 if ($filename !~ /^\._/) {
492                                         Sesse::pr0n::Common::ensure_cached($r, $filename, $newid, -1, -1, 1, 80, 64, 320, 256, -1, -1);
493                                 }
494                                 
495                                 # OK, we got this far, commit
496                                 $dbh->commit;
497
498                                 $r->log->notice("Successfully wrote $event/$filename to $fname");
499                         };
500                         if ($@) {
501                                 # Some error occurred, rollback and bomb out
502                                 $dbh->rollback;
503                                 dberror($r, "Transaction aborted because $@");
504                                 unlink($fname);
505                         }
506                 }
507
508                 # Insert a `shadow file' we can stat the next 30 secs
509                 if (defined($autorename) && $autorename eq "autorename/") {
510                         $dbh->do('DELETE FROM shadow_files WHERE expires_at <= now() OR (event=? AND filename=?);',
511                                 undef, $event, $filename)
512                                 or dberror($r, "Couldn't prune shadow_files");
513                         $dbh->do('INSERT INTO shadow_files (event,filename,id,expires_at) VALUES (?,?,?,now() + interval \'30 seconds\');',
514                                 undef, $event, $orig_filename, $newid)
515                                 or dberror($r, "Couldn't add shadow file");
516                         $r->log->info("Added shadow entry for $event/$filename");
517                 }
518
519                 $r->content_type('text/plain; charset="utf-8"');
520                 $r->status(201);
521                 $r->print("OK");
522
523                 return Apache2::Const::OK;
524         }
525
526         # Yes, we fake locks. :-)
527         if ($r->method eq "LOCK") {
528                 if ($r->uri !~ m#^/webdav/upload/([a-zA-Z0-9-]+)/(autorename/)?([a-zA-Z0-9._-]+)$#) {
529                         $r->status(403);
530                         $r->content_type('text/plain; charset=utf-8');
531                         $r->print("No access");
532                         return Apache2::Const::OK;
533                 }
534
535                 my ($event, $autorename, $filename) = ($1, $2, $3);
536                 my $sha1 = Digest::SHA1::sha1_base64("/$event/$autorename/$filename");
537
538                 $r->status(200);
539                 $r->content_type('text/xml; charset=utf-8');
540
541                 $r->print(<<"EOF");
542 <?xml version="1.0" encoding="utf-8"?>
543 <prop xmlns="DAV:">
544   <lockdiscovery>
545     <activelock>
546       <locktype><write/></locktype>
547       <lockscope><exclusive/></lockscope>
548       <depth>0</depth>
549       <owner>
550         <href>/webdav/upload/$event/$autorename$filename</href>
551       </owner>
552       <timeout>Second-3600</timeout>
553       <locktoken>
554         <href>opaquelocktoken:$sha1</href>
555       </locktoken>
556     </activelock>
557   </lockdiscovery>
558 </prop>
559 EOF
560                 return Apache2::Const::OK;
561         }
562         
563         if ($r->method eq "UNLOCK") {
564                 $r->content_type('text/plain; charset="utf-8"');
565                 $r->status(200);
566                 $r->print("OK");
567
568                 return Apache2::Const::OK;
569         }
570
571         if ($r->method eq "DELETE") {
572                 if ($r->uri !~ m#^/webdav/upload/([a-zA-Z0-9-]+)/(autorename/)?(\._[a-zA-Z0-9._-]+)$#) {
573                         $r->status(403);
574                         $r->content_type('text/plain; charset=utf-8');
575                         $r->print("No access");
576                         return Apache2::Const::OK;
577                 }
578                 
579                 my ($event, $autorename, $filename) = ($1, $2, $3);
580                 $dbh->do('DELETE FROM images WHERE event=? AND filename=?;',
581                         undef, $event, $filename)
582                         or dberror($r, "Couldn't remove file");
583                 $dbh->do('UPDATE events SET last_update=CURRENT_TIMESTAMP WHERE id=?',
584                         undef, $event)
585                         or dberror($r, "Couldn't invalidate cache");
586                 $r->status(200);
587                 $r->print("OK");
588
589                 $r->log->info("deleted $event/$filename");
590                 
591                 return Apache2::Const::OK;
592         }
593         
594         if ($r->method eq "MOVE" or
595             $r->method eq "MKCOL" or
596             $r->method eq "RMCOL" or
597             $r->method eq "RENAME" or
598             $r->method eq "COPY") {
599                 $r->content_type('text/plain; charset="utf-8"');
600                 $r->status(403);
601                 $r->print("Sorry, you do not have access to that feature.");
602                 return Apache2::Const::OK;
603         }
604         
605         $r->content_type('text/plain; charset=utf-8');
606         $r->log->error("unknown method " . $r->method);
607         $r->status(500);
608         $r->print("Unknown method");
609         
610         return Apache2::Const::OK;
611 }
612
613 1;
614
615