123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424 |
- # Copyright (C) 2005–2015 Alex Schroeder <alex@emacswiki.org>
- # Copyright (C) 2014–2015 Aleks-Daniel Jakimenko <alex.jakimenko@gmail.com>
- # 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/>.
- use strict;
- use v5.10;
- AddModuleDescription('webdav.pl', 'WebDAV Extension');
- our ($q, $Now, %Page, @KnownLocks, $DataDir);
- our ($WebDavCache);
- $WebDavCache = "$DataDir/webdav";
- push(@KnownLocks, 'webdav');
- use CGI;
- *DavOldDoBrowseRequest = \&DoBrowseRequest;
- *DoBrowseRequest = \&DavNewDoBrowseRequest;
- 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};
- }
- package OddMuse::DAV;
- use strict;
- use warnings;
- no warnings 'once'; # TODO Name "OddMuse::Var" used only once: possible typo ... ?
- use HTTP::Date qw(time2str time2isoz);
- use XML::LibXML;
- use Digest::MD5 qw(md5_base64);
- my $verbose = 0;
- # These are the methods we understand -- but not all of them are truly
- # implemented.
- our %implemented = (
- get => 1,
- head => 1,
- options => 1,
- propfind => 1,
- put => 1,
- trace => 1,
- lock => 1,
- unlock => 1,
- );
- sub new {
- my ($class) = @_;
- my $self = {};
- bless $self, $class;
- return $self;
- }
- sub run {
- my ($self, $q) = @_;
- my $path = $q->path_info;
- return 0 if $path !~ m|/dav|;
- my $method = $q->request_method;
- $method = lc $method;
- warn uc $method, " ", $path, "\n" if $verbose;
- if (not $implemented{$method}) {
- print $q->header( -status => '501 Not Implemented', );
- return 1;
- }
- $self->$method($q);
- return 1;
- }
- sub options {
- my ($self, $q) = @_;
- print $q->header( -allow => join(',', map { uc } keys %implemented),
- -DAV => 1,
- -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
- }
- 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",
- -status => "200 OK",);
- print $OddMuse::Page{text} unless $head;
- }
- } else {
- print $q->header( -status => "404 Not Found", );
- print OddMuse::NewText($id) unless $head;
- }
- }
- sub put {
- my ($self, $q) = @_;
- my $id = OddMuse::GetId();
- my $type = $ENV{'CONTENT_TYPE'};
- my $text = body();
- # 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);
- OddMuse::SetParam('summary', OddMuse::Ts('Upload of %s file', $type));
- }
- 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!
- # warn $data;
- return $data;
- }
- sub no_content {
- warn "RESPONSE: 204\n\n" if $verbose;
- print CGI::header( -status => "204 No Content", );
- }
- sub created {
- warn "RESPONSE: 201\n\n" if $verbose;
- print CGI::header( -status => "201 Created", );
- }
- sub propfind {
- my ($self, $q) = @_;
- my $depth = $q->http('depth') || "infinity";
- warn "depth: $depth\n" if $verbose;
- my $content = body();
- # warn "content: $content\n";
- my $parser = XML::LibXML->new;
- my $req;
- eval { $req = $parser->parse_string($content); };
- if ($@) {
- warn "RESPONSE: 400\n\n" if $verbose;
- 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;
- }
- # 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 ];
- }
- }
- # warn "reqprops: " . join(", ", map {join "", @$_} @reqprops) . "\n";
- # collection only, all pages, or single page?
- my @pages = OddMuse::AllPagesList();
- if ($q->path_info =~ '^/dav/?$') {
- # warn "collection!\n";
- if ($depth eq "0") {
- # warn "only the collection!\n";
- @pages = ('');
- } else {
- # warn "all pages!\n";
- unshift(@pages, '');
- }
- } else {
- my $id = OddMuse::GetId();
- # warn "single page, id: $id\n";
- if (not $OddMuse::IndexHash{$id}) {
- warn "RESPONSE: 404\n\n" if $verbose;
- print $q->header( -status => "404 Not Found", );
- print OddMuse::NewText($id);
- return;
- }
- @pages = ($id);
- }
- print $q->header( -status => "207 Multi-Status",
- -etag => md5_base64($OddMuse::LastUpdate
- . $req->toString)
- );
- 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();
- for my $id (@pages) {
- my $title = $id;
- $title =~ s/_/ /g;
- 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 ||= '';
- my $resp = $doc->createElement('D:response');
- $multistat->addChild($resp);
- my $href = $doc->createElement('D:href');
- $href->appendText($OddMuse::ScriptName . '/dav/' . OddMuse::UrlEncode($id));
- $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');
- $prop->appendText('text/plain');
- $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');
- if (not $id) { # change for namespaces later
- my $col = $doc->createElement('D:collection');
- $prop->addChild($col);
- }
- $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);
- } else {
- my $prefix = $prefixes{$ns};
- if (!defined $prefix) {
- $prefix = 'i' . $i++;
- # mod_dav sets <response> 'xmlns' attribute - whatever
- #$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);
- } 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');
- $prop->appendText('text/plain');
- $okprops->addChild($prop);
- $prop = $doc->createElement('D:getlastmodified');
- $prop->appendText($mtime);
- $okprops->addChild($prop);
- $prop = $doc->createElement('D:resourcetype');
- if (not $id) { # change for namespaces later
- my $col = $doc->createElement('D:collection');
- $prop->addChild($col);
- }
- $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);
- }
- 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;
- 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;
- }
|