Files
oddmuse/modules/webdav.pl

424 lines
13 KiB
Perl
Raw Normal View History

# Copyright (C) 20052015 Alex Schroeder <alex@emacswiki.org>
# Copyright (C) 20142015 Aleks-Daniel Jakimenko <alex.jakimenko@gmail.com>
2005-08-26 23:52:20 +00:00
# Copyright (C) 2004, Leon Brocard
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.
2005-08-26 23:52:20 +00:00
use strict;
use v5.10;
AddModuleDescription('webdav.pl', 'WebDAV Extension');
2015-04-10 13:31:28 +03:00
our ($q, $Now, %Page, @KnownLocks, $DataDir);
our ($WebDavCache);
$WebDavCache = "$DataDir/webdav";
push(@KnownLocks, 'webdav');
2005-08-28 20:19:57 +00:00
2005-08-26 23:52:20 +00:00
use CGI;
*DavOldDoBrowseRequest = \&DoBrowseRequest;
*DoBrowseRequest = \&DavNewDoBrowseRequest;
2005-08-26 23:52:20 +00:00
sub DavNewDoBrowseRequest {
my $dav = new OddMuse::DAV;
$dav->run($q)||DavOldDoBrowseRequest();
}
*DavOldOpenPage = \&OpenPage;
*OpenPage = \&DavNewOpenPage;
sub DavNewOpenPage {
DavOldOpenPage(@_);
$Page{created} = $Now unless $Page{created} or $Page{revision};
}
2005-08-26 23:52:20 +00:00
package OddMuse::DAV;
2005-08-26 23:52:20 +00:00
use strict;
use warnings;
no warnings 'once'; # TODO Name "OddMuse::Var" used only once: possible typo ... ?
2005-08-28 19:21:27 +00:00
use HTTP::Date qw(time2str time2isoz);
2005-08-26 23:52:20 +00:00
use XML::LibXML;
use Digest::MD5 qw(md5_base64);
2005-08-26 23:52:20 +00:00
my $verbose = 0;
2005-08-26 23:52:20 +00:00
# These are the methods we understand -- but not all of them are truly
# implemented.
our %implemented = (
get => 1,
head => 1,
2005-08-28 19:09:09 +00:00
options => 1,
2005-08-26 23:52:20 +00:00
propfind => 1,
2005-08-28 19:09:09 +00:00
put => 1,
trace => 1,
lock => 1,
2005-08-26 23:52:20 +00:00
unlock => 1,
);
sub new {
my ($class) = @_;
my $self = {};
bless $self, $class;
return $self;
}
sub run {
my ($self, $q) = @_;
my $path = $q->path_info;
2005-08-28 19:09:09 +00:00
return 0 if $path !~ m|/dav|;
2005-08-26 23:52:20 +00:00
my $method = $q->request_method;
$method = lc $method;
warn uc $method, " ", $path, "\n" if $verbose;
2005-08-28 19:09:09 +00:00
if (not $implemented{$method}) {
print $q->header( -status => '501 Not Implemented', );
return 1;
}
2005-08-26 23:52:20 +00:00
$self->$method($q);
return 1;
}
sub options {
my ($self, $q) = @_;
print $q->header( -allow => join(',', map { uc } keys %implemented),
-DAV => 1,
2005-08-26 23:52:20 +00:00
-status => "200 OK", );
}
sub lock {
my ($self, $q) = @_;
print $q->header( -status => "412 Precondition Failed", ); # fake it
}
sub unlock {
my ($self, $q) = @_;
print $q->header( -status => "204 No Content", ); # fake it
}
2005-08-26 23:52:20 +00:00
sub head {
get(@_, 1);
}
sub get {
my ($self, $q, $head) = @_;
my $id = OddMuse::GetId();
OddMuse::AllPagesList();
if ($OddMuse::IndexHash{$id}) {
OddMuse::OpenPage($id);
if (OddMuse::FileFresh()) {
print $q->header( -status => '304 Not Modified', );
} else {
print $q->header( -cache_control => 'max-age=10',
-etag => $OddMuse::Page{ts},
-type => "text/plain; charset=UTF-8",
2005-08-28 19:48:56 +00:00
-status => "200 OK",);
2005-08-26 23:52:20 +00:00
print $OddMuse::Page{text} unless $head;
}
} else {
print $q->header( -status => "404 Not Found", );
print OddMuse::NewText($id) unless $head;
2005-08-26 23:52:20 +00:00
}
}
sub put {
my ($self, $q) = @_;
2005-08-28 19:09:09 +00:00
my $id = OddMuse::GetId();
my $type = $ENV{'CONTENT_TYPE'};
2005-08-28 19:09:09 +00:00
my $text = body();
2005-08-29 20:58:17 +00:00
# hard coded magic based on the specs
if (not $type) {
if (substr($text,0,4) eq "\377\330\377\340"
or substr($text,0,4) eq "\377\330\377\341") {
# http://www.itworld.com/nl/unix_insider/07072005/
$type = "image/jpeg";
} elsif (substr($text,0,8) eq "\211\120\116\107\15\12\32\12") {
# http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html
$type = "image/png";
}
}
# warn $type;
if ($type and substr($type,0,5) ne 'text/') {
require MIME::Base64;
$text = '#FILE ' . $type . "\n" . MIME::Base64::encode($text);
2005-08-29 20:47:28 +00:00
OddMuse::SetParam('summary', OddMuse::Ts('Upload of %s file', $type));
}
2005-08-28 19:09:09 +00:00
OddMuse::SetParam('text', $text);
local *OddMuse::ReBrowsePage;
OddMuse::AllPagesList();
if ($OddMuse::IndexHash{$id}) {
*OddMuse::ReBrowsePage = \&no_content; # modified existing page
} else {
*OddMuse::ReBrowsePage = \&created; # created new page
}
OddMuse::DoPost($id); # do the real posting
}
sub body {
local $/; # slurp
my $data = <STDIN>; # can only be read once!
2006-09-12 16:39:58 +00:00
# warn $data;
return $data;
}
sub no_content {
warn "RESPONSE: 204\n\n" if $verbose;
print CGI::header( -status => "204 No Content", );
2005-08-26 23:52:20 +00:00
}
2005-08-28 19:09:09 +00:00
sub created {
warn "RESPONSE: 201\n\n" if $verbose;
2005-08-28 19:09:09 +00:00
print CGI::header( -status => "201 Created", );
2005-08-26 23:52:20 +00:00
}
sub propfind {
my ($self, $q) = @_;
my $depth = $q->http('depth') || "infinity";
warn "depth: $depth\n" if $verbose;
2005-08-26 23:52:20 +00:00
my $content = body();
2005-08-28 21:04:55 +00:00
# warn "content: $content\n";
2005-08-26 23:52:20 +00:00
my $parser = XML::LibXML->new;
my $req;
eval { $req = $parser->parse_string($content); };
if ($@) {
warn "RESPONSE: 400\n\n" if $verbose;
2005-08-26 23:52:20 +00:00
print $q->header( -status => "400 Bad Request", );
print $@;
return;
}
# warn "req: " . $req->toString;
# the spec says the the reponse should not be cached...
if ($q->http('HTTP_IF_NONE_MATCH') and GetParam('cache', $OddMuse::UseCache) >= 2
and $q->http('HTTP_IF_NONE_MATCH') eq md5_base64($OddMuse::LastUpdate
. $req->toString)) {
warn "RESPONSE: 304\n\n" if $verbose;
print $q->header( -status => '304 Not Modified', );
return;
}
2005-08-26 23:52:20 +00:00
# what properties do we need?
my $reqinfo;
my @reqprops;
$reqinfo = $req->find('/*/*')->shift->localname;
if ($reqinfo eq 'prop') {
for my $node ($req->find('/*/*/*')->get_nodelist) {
push @reqprops, [ $node->namespaceURI, $node->localname ];
}
}
2005-08-28 19:09:09 +00:00
# warn "reqprops: " . join(", ", map {join "", @$_} @reqprops) . "\n";
2005-08-26 23:52:20 +00:00
# collection only, all pages, or single page?
my @pages = OddMuse::AllPagesList();
if ($q->path_info =~ '^/dav/?$') {
2005-08-28 19:09:09 +00:00
# warn "collection!\n";
if ($depth eq "0") {
# warn "only the collection!\n";
@pages = ('');
2005-08-26 23:52:20 +00:00
} else {
2005-08-28 19:09:09 +00:00
# warn "all pages!\n";
unshift(@pages, '');
2005-08-26 23:52:20 +00:00
}
} else {
my $id = OddMuse::GetId();
2005-08-28 19:09:09 +00:00
# warn "single page, id: $id\n";
2005-08-26 23:52:20 +00:00
if (not $OddMuse::IndexHash{$id}) {
warn "RESPONSE: 404\n\n" if $verbose;
2005-08-26 23:52:20 +00:00
print $q->header( -status => "404 Not Found", );
print OddMuse::NewText($id);
2005-08-26 23:52:20 +00:00
return;
}
@pages = ($id);
}
print $q->header( -status => "207 Multi-Status",
-etag => md5_base64($OddMuse::LastUpdate
. $req->toString)
);
2005-08-26 23:52:20 +00:00
my $doc = XML::LibXML::Document->new('1.0', 'utf-8');
my $multistat = $doc->createElement('D:multistatus');
$multistat->setAttribute('xmlns:D', 'DAV:');
$doc->setDocumentElement($multistat);
my %data = propfind_data();
2005-08-26 23:52:20 +00:00
for my $id (@pages) {
my $title = $id;
$title =~ s/_/ /g;
2005-08-28 19:09:09 +00:00
my ($size, $mtime, $ctime) = ('', '', ''); # undefined for the wiki proper ($id eq '')
($size, $mtime, $ctime) = @{$data{$id}} if $id;
my $etag = $mtime; # $mtime is $Page{ts} which is used as etag in GET
# modified time is stringified human readable HTTP::Date style
$mtime = time2str($mtime);
# created time is ISO format
# tidy up date format - isoz isn't exactly what we want, but
# it's easy to change.
$ctime = time2isoz($ctime);
$ctime =~ s/ /T/;
$ctime =~ s/Z//;
# force empty strings if undefined
$size ||= '';
2005-08-26 23:52:20 +00:00
my $resp = $doc->createElement('D:response');
$multistat->addChild($resp);
my $href = $doc->createElement('D:href');
$href->appendText($OddMuse::ScriptName . '/dav/' . OddMuse::UrlEncode($id));
2005-08-26 23:52:20 +00:00
$resp->addChild($href);
my $okprops = $doc->createElement('D:prop');
my $nfprops = $doc->createElement('D:prop');
my $prop;
if ($reqinfo eq 'prop') {
my %prefixes = ('DAV:' => 'D');
my $i = 0;
for my $reqprop (@reqprops) {
my ($ns, $name) = @$reqprop;
if ($ns eq 'DAV:' && $name eq 'creationdate') {
$prop = $doc->createElement('D:creationdate');
$prop->appendText($ctime);
$okprops->addChild($prop);
} elsif ($ns eq 'DAV:' && $name eq 'getcontentlength') {
$prop = $doc->createElement('D:getcontentlength');
$prop->appendText($size);
$okprops->addChild($prop);
} elsif ($ns eq 'DAV:' && $name eq 'getcontenttype') {
$prop = $doc->createElement('D:getcontenttype');
2005-08-28 19:09:09 +00:00
$prop->appendText('text/plain');
2005-08-26 23:52:20 +00:00
$okprops->addChild($prop);
} elsif ($ns eq 'DAV:' && $name eq 'getlastmodified') {
$prop = $doc->createElement('D:getlastmodified');
$prop->appendText($mtime);
$okprops->addChild($prop);
} elsif ($ns eq 'DAV:' && $name eq 'resourcetype') {
$prop = $doc->createElement('D:resourcetype');
2005-08-28 19:09:09 +00:00
if (not $id) { # change for namespaces later
my $col = $doc->createElement('D:collection');
$prop->addChild($col);
}
2005-08-26 23:52:20 +00:00
$okprops->addChild($prop);
} elsif ($ns eq 'DAV:' && $name eq 'displayname') {
$prop = $doc->createElement('D:displayname');
$prop->appendText($title);
$okprops->addChild($prop);
} elsif ($ns eq 'DAV:' && $name eq 'getetag') {
$prop = $doc->createElement('D:getetag');
$prop->appendText($etag);
$okprops->addChild($prop);
2005-08-26 23:52:20 +00:00
} else {
my $prefix = $prefixes{$ns};
if (!defined $prefix) {
$prefix = 'i' . $i++;
# mod_dav sets <response> 'xmlns' attribute - whatever
2005-08-26 23:52:20 +00:00
#$nfprops->setAttribute("xmlns:$prefix", $ns);
$resp->setAttribute("xmlns:$prefix", $ns);
$prefixes{$ns} = $prefix;
}
$prop = $doc->createElement("$prefix:$name");
$nfprops->addChild($prop);
}
}
} elsif ($reqinfo eq 'propname') {
$prop = $doc->createElement('D:creationdate');
$okprops->addChild($prop);
$prop = $doc->createElement('D:getcontentlength');
$okprops->addChild($prop);
$prop = $doc->createElement('D:getcontenttype');
$okprops->addChild($prop);
$prop = $doc->createElement('D:getlastmodified');
$okprops->addChild($prop);
$prop = $doc->createElement('D:resourcetype');
$okprops->addChild($prop);
$prop = $doc->createElement('D:displayname');
$okprops->addChild($prop);
$prop = $doc->createElement('D:getetag');
$okprops->addChild($prop);
2005-08-26 23:52:20 +00:00
} else {
$prop = $doc->createElement('D:creationdate');
$prop->appendText($ctime);
$okprops->addChild($prop);
$prop = $doc->createElement('D:getcontentlength');
$prop->appendText($size);
$okprops->addChild($prop);
$prop = $doc->createElement('D:getcontenttype');
2005-08-28 19:09:09 +00:00
$prop->appendText('text/plain');
2005-08-26 23:52:20 +00:00
$okprops->addChild($prop);
$prop = $doc->createElement('D:getlastmodified');
$prop->appendText($mtime);
$okprops->addChild($prop);
$prop = $doc->createElement('D:resourcetype');
2005-08-28 19:09:09 +00:00
if (not $id) { # change for namespaces later
my $col = $doc->createElement('D:collection');
$prop->addChild($col);
}
2005-08-26 23:52:20 +00:00
$okprops->addChild($prop);
$prop = $doc->createElement('D:displayname');
$prop->appendText($title);
$okprops->addChild($prop);
$prop = $doc->createElement('D:getetag');
$prop->appendText($etag);
$okprops->addChild($prop);
2005-08-26 23:52:20 +00:00
}
2005-08-26 23:52:20 +00:00
if ($okprops->hasChildNodes) {
my $propstat = $doc->createElement('D:propstat');
$propstat->addChild($okprops);
my $stat = $doc->createElement('D:status');
$stat->appendText('HTTP/1.1 200 OK');
$propstat->addChild($stat);
$resp->addChild($propstat);
}
if ($nfprops->hasChildNodes) {
my $propstat = $doc->createElement('D:propstat');
$propstat->addChild($nfprops);
my $stat = $doc->createElement('D:status');
$stat->appendText('HTTP/1.1 404 Not Found');
$propstat->addChild($stat);
$resp->addChild($propstat);
}
}
warn "RESPONSE: 207\n" . $doc->toString(1) . "\n" if $verbose;
2005-08-26 23:52:20 +00:00
print $doc->toString(1);
}
sub propfind_data {
my %data = ();
my $update = Modified($OddMuse::WebDavCache);
if ($update and $OddMuse::LastUpdate == $update) {
my $data = OddMuse::ReadFileOrDie($OddMuse::WebDavCache);
map {
my ($id, @attr) = split(/$OddMuse::FS/, $_);
$data{$id} = \@attr;
} split(/\n/, $data);
} else {
my @pages = OddMuse::AllPagesList();
my $cache = '';
foreach my $id (@pages) {
OddMuse::OpenPage($id);
my ($size, $mtime, $ctime);
$size = length($OddMuse::Page{text}||0);
$mtime = $OddMuse::Page{ts}||0;
$ctime = $OddMuse::Page{created}||0;
$data{$id} = [$size, $mtime, $ctime];
$cache .= join($OddMuse::FS, $id, $size, $mtime, $ctime) . "\n";
}
if (OddMuse::RequestLockDir('webdav')) { # not fatal
OddMuse::WriteStringToFile($OddMuse::WebDavCache, $cache);
utime $OddMuse::LastUpdate, $OddMuse::LastUpdate, $OddMuse::WebDavCache; # touch index file
OddMuse::ReleaseLockDir('webdav');
}
}
return %data;
}