webdav.pl 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. # Copyright (C) 2005–2015 Alex Schroeder <alex@emacswiki.org>
  2. # Copyright (C) 2014–2015 Aleks-Daniel Jakimenko <alex.jakimenko@gmail.com>
  3. # Copyright (C) 2004, Leon Brocard
  4. #
  5. # This program is free software; you can redistribute it and/or modify it under
  6. # the terms of the GNU General Public License as published by the Free Software
  7. # Foundation; either version 3 of the License, or (at your option) any later
  8. # version.
  9. #
  10. # This program is distributed in the hope that it will be useful, but WITHOUT
  11. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  12. # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License along with
  15. # this program. If not, see <http://www.gnu.org/licenses/>.
  16. use strict;
  17. use v5.10;
  18. AddModuleDescription('webdav.pl', 'WebDAV Extension');
  19. our ($q, $Now, %Page, @KnownLocks, $DataDir);
  20. our ($WebDavCache);
  21. $WebDavCache = "$DataDir/webdav";
  22. push(@KnownLocks, 'webdav');
  23. *DavOldDoBrowseRequest = \&DoBrowseRequest;
  24. *DoBrowseRequest = \&DavNewDoBrowseRequest;
  25. sub DavNewDoBrowseRequest {
  26. my $dav = new OddMuse::DAV;
  27. $dav->run($q)||DavOldDoBrowseRequest();
  28. }
  29. *DavOldOpenPage = \&OpenPage;
  30. *OpenPage = \&DavNewOpenPage;
  31. sub DavNewOpenPage {
  32. DavOldOpenPage(@_);
  33. $Page{created} = $Now unless $Page{created} or $Page{revision};
  34. }
  35. package OddMuse::DAV;
  36. use strict;
  37. use warnings;
  38. no warnings 'once'; # TODO Name "OddMuse::Var" used only once: possible typo ... ?
  39. use HTTP::Date qw(time2str time2isoz);
  40. use XML::LibXML;
  41. use Digest::MD5 qw(md5_base64);
  42. my $verbose = 0;
  43. # These are the methods we understand -- but not all of them are truly
  44. # implemented.
  45. our %implemented = (
  46. get => 1,
  47. head => 1,
  48. options => 1,
  49. propfind => 1,
  50. put => 1,
  51. trace => 1,
  52. lock => 1,
  53. unlock => 1,
  54. );
  55. sub new {
  56. my ($class) = @_;
  57. my $self = {};
  58. bless $self, $class;
  59. return $self;
  60. }
  61. sub run {
  62. my ($self, $q) = @_;
  63. my $path = $q->path_info;
  64. return 0 if $path !~ m|/dav|;
  65. my $method = $q->request_method;
  66. $method = lc $method;
  67. warn uc $method, " ", $path, "\n" if $verbose;
  68. if (not $implemented{$method}) {
  69. print $q->header( -status => '501 Not Implemented', );
  70. return 1;
  71. }
  72. $self->$method($q);
  73. return 1;
  74. }
  75. sub options {
  76. my ($self, $q) = @_;
  77. print $q->header( -allow => join(',', map { uc } keys %implemented),
  78. -DAV => 1,
  79. -status => "200 OK", );
  80. }
  81. sub lock {
  82. my ($self, $q) = @_;
  83. print $q->header( -status => "412 Precondition Failed", ); # fake it
  84. }
  85. sub unlock {
  86. my ($self, $q) = @_;
  87. print $q->header( -status => "204 No Content", ); # fake it
  88. }
  89. sub head {
  90. get(@_, 1);
  91. }
  92. sub get {
  93. my ($self, $q, $head) = @_;
  94. my $id = OddMuse::GetId();
  95. OddMuse::AllPagesList();
  96. if ($OddMuse::IndexHash{$id}) {
  97. OddMuse::OpenPage($id);
  98. if (OddMuse::FileFresh()) {
  99. print $q->header( -status => '304 Not Modified', );
  100. } else {
  101. print $q->header( -cache_control => 'max-age=10',
  102. -etag => $OddMuse::Page{ts},
  103. -type => "text/plain; charset=UTF-8",
  104. -status => "200 OK",);
  105. print $OddMuse::Page{text} unless $head;
  106. }
  107. } else {
  108. print $q->header( -status => "404 Not Found", );
  109. print OddMuse::NewText($id) unless $head;
  110. }
  111. }
  112. sub put {
  113. my ($self, $q) = @_;
  114. my $id = OddMuse::GetId();
  115. my $type = $ENV{'CONTENT_TYPE'};
  116. my $text = $q->param('PUTDATA'); # CGI.pm does that!
  117. # warn "text: $text\n";
  118. # hard coded magic based on the specs
  119. if (not $type) {
  120. if (substr($text,0,4) eq "\377\330\377\340"
  121. or substr($text,0,4) eq "\377\330\377\341") {
  122. # http://www.itworld.com/nl/unix_insider/07072005/
  123. $type = "image/jpeg";
  124. } elsif (substr($text,0,8) eq "\211\120\116\107\15\12\32\12") {
  125. # http://www.libpng.org/pub/png/spec/1.2/PNG-Structure.html
  126. $type = "image/png";
  127. }
  128. }
  129. # warn $type;
  130. if ($type and substr($type,0,5) ne 'text/') {
  131. require MIME::Base64;
  132. $text = '#FILE ' . $type . "\n" . MIME::Base64::encode($text);
  133. OddMuse::SetParam('summary', OddMuse::Ts('Upload of %s file', $type));
  134. }
  135. OddMuse::SetParam('text', $text);
  136. local *OddMuse::ReBrowsePage;
  137. OddMuse::AllPagesList();
  138. if ($OddMuse::IndexHash{$id}) {
  139. *OddMuse::ReBrowsePage = \&no_content; # modified existing page
  140. } else {
  141. *OddMuse::ReBrowsePage = \&created; # created new page
  142. }
  143. OddMuse::DoPost($id); # do the real posting
  144. }
  145. sub no_content {
  146. warn "RESPONSE: 204\n\n" if $verbose;
  147. print CGI::header( -status => "204 No Content", );
  148. }
  149. sub created {
  150. warn "RESPONSE: 201\n\n" if $verbose;
  151. print CGI::header( -status => "201 Created", );
  152. }
  153. sub propfind {
  154. my ($self, $q) = @_;
  155. my $depth = $q->http('depth') || "infinity";
  156. warn "depth: $depth\n" if $verbose;
  157. # only PUT and POST are handled by CGI; for PROPFIND we need to read the body
  158. # ourselves
  159. local $/; # slurp
  160. my $content = <STDIN>;
  161. warn "PROFIND $content\n" if $verbose;
  162. my $parser = XML::LibXML->new;
  163. my $req;
  164. eval { $req = $parser->parse_string($content); };
  165. if ($@) {
  166. warn "RESPONSE: 400\n\n" if $verbose;
  167. print $q->header( -status => "400 Bad Request", );
  168. print $@;
  169. return;
  170. }
  171. # warn "req: " . $req->toString;
  172. # the spec says the the reponse should not be cached...
  173. if ($q->http('HTTP_IF_NONE_MATCH') and GetParam('cache', $OddMuse::UseCache) >= 2
  174. and $q->http('HTTP_IF_NONE_MATCH') eq md5_base64($OddMuse::LastUpdate
  175. . $req->toString)) {
  176. warn "RESPONSE: 304\n\n" if $verbose;
  177. print $q->header( -status => '304 Not Modified', );
  178. return;
  179. }
  180. # what properties do we need?
  181. my $reqinfo;
  182. my @reqprops;
  183. $reqinfo = $req->find('/*/*')->shift->localname;
  184. if ($reqinfo eq 'prop') {
  185. for my $node ($req->find('/*/*/*')->get_nodelist) {
  186. push @reqprops, [ $node->namespaceURI, $node->localname ];
  187. }
  188. }
  189. # warn "reqprops: " . join(", ", map {join "", @$_} @reqprops) . "\n";
  190. # collection only, all pages, or single page?
  191. my @pages = OddMuse::AllPagesList();
  192. if ($q->path_info =~ '^/dav/?$') {
  193. # warn "collection!\n";
  194. if ($depth eq "0") {
  195. # warn "only the collection!\n";
  196. @pages = ('');
  197. } else {
  198. # warn "all pages!\n";
  199. unshift(@pages, '');
  200. }
  201. } else {
  202. my $id = OddMuse::GetId();
  203. # warn "single page, id: $id\n";
  204. if (not $OddMuse::IndexHash{$id}) {
  205. warn "RESPONSE: 404\n\n" if $verbose;
  206. print $q->header( -status => "404 Not Found", );
  207. print OddMuse::NewText($id);
  208. return;
  209. }
  210. @pages = ($id);
  211. }
  212. print $q->header( -status => "207 Multi-Status",
  213. -etag => md5_base64($OddMuse::LastUpdate
  214. . $req->toString)
  215. );
  216. my $doc = XML::LibXML::Document->new('1.0', 'utf-8');
  217. my $multistat = $doc->createElement('D:multistatus');
  218. $multistat->setAttribute('xmlns:D', 'DAV:');
  219. $doc->setDocumentElement($multistat);
  220. my %data = propfind_data();
  221. for my $id (@pages) {
  222. my $title = $id;
  223. $title =~ s/_/ /g;
  224. my ($size, $mtime, $ctime) = ('', '', ''); # undefined for the wiki proper ($id eq '')
  225. ($size, $mtime, $ctime) = @{$data{$id}} if $id;
  226. my $etag = $mtime; # $mtime is $Page{ts} which is used as etag in GET
  227. # modified time is stringified human readable HTTP::Date style
  228. $mtime = time2str($mtime);
  229. # created time is ISO format
  230. # tidy up date format - isoz isn't exactly what we want, but
  231. # it's easy to change.
  232. $ctime = time2isoz($ctime);
  233. $ctime =~ s/ /T/;
  234. $ctime =~ s/Z//;
  235. # force empty strings if undefined
  236. $size ||= '';
  237. my $resp = $doc->createElement('D:response');
  238. $multistat->addChild($resp);
  239. my $href = $doc->createElement('D:href');
  240. $href->appendText($OddMuse::ScriptName . '/dav/' . OddMuse::UrlEncode($id));
  241. $resp->addChild($href);
  242. my $okprops = $doc->createElement('D:prop');
  243. my $nfprops = $doc->createElement('D:prop');
  244. my $prop;
  245. if ($reqinfo eq 'prop') {
  246. my %prefixes = ('DAV:' => 'D');
  247. my $i = 0;
  248. for my $reqprop (@reqprops) {
  249. my ($ns, $name) = @$reqprop;
  250. if ($ns eq 'DAV:' && $name eq 'creationdate') {
  251. $prop = $doc->createElement('D:creationdate');
  252. $prop->appendText($ctime);
  253. $okprops->addChild($prop);
  254. } elsif ($ns eq 'DAV:' && $name eq 'getcontentlength') {
  255. $prop = $doc->createElement('D:getcontentlength');
  256. $prop->appendText($size);
  257. $okprops->addChild($prop);
  258. } elsif ($ns eq 'DAV:' && $name eq 'getcontenttype') {
  259. $prop = $doc->createElement('D:getcontenttype');
  260. $prop->appendText('text/plain');
  261. $okprops->addChild($prop);
  262. } elsif ($ns eq 'DAV:' && $name eq 'getlastmodified') {
  263. $prop = $doc->createElement('D:getlastmodified');
  264. $prop->appendText($mtime);
  265. $okprops->addChild($prop);
  266. } elsif ($ns eq 'DAV:' && $name eq 'resourcetype') {
  267. $prop = $doc->createElement('D:resourcetype');
  268. if (not $id) { # change for namespaces later
  269. my $col = $doc->createElement('D:collection');
  270. $prop->addChild($col);
  271. }
  272. $okprops->addChild($prop);
  273. } elsif ($ns eq 'DAV:' && $name eq 'displayname') {
  274. $prop = $doc->createElement('D:displayname');
  275. $prop->appendText($title);
  276. $okprops->addChild($prop);
  277. } elsif ($ns eq 'DAV:' && $name eq 'getetag') {
  278. $prop = $doc->createElement('D:getetag');
  279. $prop->appendText($etag);
  280. $okprops->addChild($prop);
  281. } else {
  282. my $prefix = $prefixes{$ns};
  283. if (!defined $prefix) {
  284. $prefix = 'i' . $i++;
  285. # mod_dav sets <response> 'xmlns' attribute - whatever
  286. #$nfprops->setAttribute("xmlns:$prefix", $ns);
  287. $resp->setAttribute("xmlns:$prefix", $ns);
  288. $prefixes{$ns} = $prefix;
  289. }
  290. $prop = $doc->createElement("$prefix:$name");
  291. $nfprops->addChild($prop);
  292. }
  293. }
  294. } elsif ($reqinfo eq 'propname') {
  295. $prop = $doc->createElement('D:creationdate');
  296. $okprops->addChild($prop);
  297. $prop = $doc->createElement('D:getcontentlength');
  298. $okprops->addChild($prop);
  299. $prop = $doc->createElement('D:getcontenttype');
  300. $okprops->addChild($prop);
  301. $prop = $doc->createElement('D:getlastmodified');
  302. $okprops->addChild($prop);
  303. $prop = $doc->createElement('D:resourcetype');
  304. $okprops->addChild($prop);
  305. $prop = $doc->createElement('D:displayname');
  306. $okprops->addChild($prop);
  307. $prop = $doc->createElement('D:getetag');
  308. $okprops->addChild($prop);
  309. } else {
  310. $prop = $doc->createElement('D:creationdate');
  311. $prop->appendText($ctime);
  312. $okprops->addChild($prop);
  313. $prop = $doc->createElement('D:getcontentlength');
  314. $prop->appendText($size);
  315. $okprops->addChild($prop);
  316. $prop = $doc->createElement('D:getcontenttype');
  317. $prop->appendText('text/plain');
  318. $okprops->addChild($prop);
  319. $prop = $doc->createElement('D:getlastmodified');
  320. $prop->appendText($mtime);
  321. $okprops->addChild($prop);
  322. $prop = $doc->createElement('D:resourcetype');
  323. if (not $id) { # change for namespaces later
  324. my $col = $doc->createElement('D:collection');
  325. $prop->addChild($col);
  326. }
  327. $okprops->addChild($prop);
  328. $prop = $doc->createElement('D:displayname');
  329. $prop->appendText($title);
  330. $okprops->addChild($prop);
  331. $prop = $doc->createElement('D:getetag');
  332. $prop->appendText($etag);
  333. $okprops->addChild($prop);
  334. }
  335. if ($okprops->hasChildNodes) {
  336. my $propstat = $doc->createElement('D:propstat');
  337. $propstat->addChild($okprops);
  338. my $stat = $doc->createElement('D:status');
  339. $stat->appendText('HTTP/1.1 200 OK');
  340. $propstat->addChild($stat);
  341. $resp->addChild($propstat);
  342. }
  343. if ($nfprops->hasChildNodes) {
  344. my $propstat = $doc->createElement('D:propstat');
  345. $propstat->addChild($nfprops);
  346. my $stat = $doc->createElement('D:status');
  347. $stat->appendText('HTTP/1.1 404 Not Found');
  348. $propstat->addChild($stat);
  349. $resp->addChild($propstat);
  350. }
  351. }
  352. warn "RESPONSE: 207\n" . $doc->toString(1) . "\n" if $verbose;
  353. print $doc->toString(1);
  354. }
  355. sub propfind_data {
  356. my %data = ();
  357. my $update = OddMuse::Modified($OddMuse::WebDavCache);
  358. if ($update and $OddMuse::LastUpdate == $update) {
  359. my $data = OddMuse::ReadFileOrDie($OddMuse::WebDavCache);
  360. map {
  361. my ($id, @attr) = split(/$OddMuse::FS/, $_);
  362. $data{$id} = \@attr;
  363. } split(/\n/, $data);
  364. } else {
  365. my @pages = OddMuse::AllPagesList();
  366. my $cache = '';
  367. foreach my $id (@pages) {
  368. OddMuse::OpenPage($id);
  369. my ($size, $mtime, $ctime);
  370. $size = length($OddMuse::Page{text}||0);
  371. $mtime = $OddMuse::Page{ts}||0;
  372. $ctime = $OddMuse::Page{created}||0;
  373. $data{$id} = [$size, $mtime, $ctime];
  374. $cache .= join($OddMuse::FS, $id, $size, $mtime, $ctime) . "\n";
  375. }
  376. if (OddMuse::RequestLockDir('webdav')) { # not fatal
  377. OddMuse::WriteStringToFile($OddMuse::WebDavCache, $cache);
  378. utime $OddMuse::LastUpdate, $OddMuse::LastUpdate, $OddMuse::WebDavCache; # touch index file
  379. OddMuse::ReleaseLockDir('webdav');
  380. }
  381. }
  382. return %data;
  383. }