webdav.pl 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  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. use CGI;
  24. *DavOldDoBrowseRequest = \&DoBrowseRequest;
  25. *DoBrowseRequest = \&DavNewDoBrowseRequest;
  26. sub DavNewDoBrowseRequest {
  27. my $dav = new OddMuse::DAV;
  28. $dav->run($q)||DavOldDoBrowseRequest();
  29. }
  30. *DavOldOpenPage = \&OpenPage;
  31. *OpenPage = \&DavNewOpenPage;
  32. sub DavNewOpenPage {
  33. DavOldOpenPage(@_);
  34. $Page{created} = $Now unless $Page{created} or $Page{revision};
  35. }
  36. package OddMuse::DAV;
  37. use strict;
  38. use warnings;
  39. no warnings 'once'; # TODO Name "OddMuse::Var" used only once: possible typo ... ?
  40. use HTTP::Date qw(time2str time2isoz);
  41. use XML::LibXML;
  42. use Digest::MD5 qw(md5_base64);
  43. my $verbose = 0;
  44. # These are the methods we understand -- but not all of them are truly
  45. # implemented.
  46. our %implemented = (
  47. get => 1,
  48. head => 1,
  49. options => 1,
  50. propfind => 1,
  51. put => 1,
  52. trace => 1,
  53. lock => 1,
  54. unlock => 1,
  55. );
  56. sub new {
  57. my ($class) = @_;
  58. my $self = {};
  59. bless $self, $class;
  60. return $self;
  61. }
  62. sub run {
  63. my ($self, $q) = @_;
  64. my $path = $q->path_info;
  65. return 0 if $path !~ m|/dav|;
  66. my $method = $q->request_method;
  67. $method = lc $method;
  68. warn uc $method, " ", $path, "\n" if $verbose;
  69. if (not $implemented{$method}) {
  70. print $q->header( -status => '501 Not Implemented', );
  71. return 1;
  72. }
  73. $self->$method($q);
  74. return 1;
  75. }
  76. sub options {
  77. my ($self, $q) = @_;
  78. print $q->header( -allow => join(',', map { uc } keys %implemented),
  79. -DAV => 1,
  80. -status => "200 OK", );
  81. }
  82. sub lock {
  83. my ($self, $q) = @_;
  84. print $q->header( -status => "412 Precondition Failed", ); # fake it
  85. }
  86. sub unlock {
  87. my ($self, $q) = @_;
  88. print $q->header( -status => "204 No Content", ); # fake it
  89. }
  90. sub head {
  91. get(@_, 1);
  92. }
  93. sub get {
  94. my ($self, $q, $head) = @_;
  95. my $id = OddMuse::GetId();
  96. OddMuse::AllPagesList();
  97. if ($OddMuse::IndexHash{$id}) {
  98. OddMuse::OpenPage($id);
  99. if (OddMuse::FileFresh()) {
  100. print $q->header( -status => '304 Not Modified', );
  101. } else {
  102. print $q->header( -cache_control => 'max-age=10',
  103. -etag => $OddMuse::Page{ts},
  104. -type => "text/plain; charset=UTF-8",
  105. -status => "200 OK",);
  106. print $OddMuse::Page{text} unless $head;
  107. }
  108. } else {
  109. print $q->header( -status => "404 Not Found", );
  110. print OddMuse::NewText($id) unless $head;
  111. }
  112. }
  113. sub put {
  114. my ($self, $q) = @_;
  115. my $id = OddMuse::GetId();
  116. my $type = $ENV{'CONTENT_TYPE'};
  117. my $text = body();
  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 body {
  146. local $/; # slurp
  147. my $data = <STDIN>; # can only be read once!
  148. # warn $data;
  149. return $data;
  150. }
  151. sub no_content {
  152. warn "RESPONSE: 204\n\n" if $verbose;
  153. print CGI::header( -status => "204 No Content", );
  154. }
  155. sub created {
  156. warn "RESPONSE: 201\n\n" if $verbose;
  157. print CGI::header( -status => "201 Created", );
  158. }
  159. sub propfind {
  160. my ($self, $q) = @_;
  161. my $depth = $q->http('depth') || "infinity";
  162. warn "depth: $depth\n" if $verbose;
  163. my $content = body();
  164. # warn "content: $content\n";
  165. my $parser = XML::LibXML->new;
  166. my $req;
  167. eval { $req = $parser->parse_string($content); };
  168. if ($@) {
  169. warn "RESPONSE: 400\n\n" if $verbose;
  170. print $q->header( -status => "400 Bad Request", );
  171. print $@;
  172. return;
  173. }
  174. # warn "req: " . $req->toString;
  175. # the spec says the the reponse should not be cached...
  176. if ($q->http('HTTP_IF_NONE_MATCH') and GetParam('cache', $OddMuse::UseCache) >= 2
  177. and $q->http('HTTP_IF_NONE_MATCH') eq md5_base64($OddMuse::LastUpdate
  178. . $req->toString)) {
  179. warn "RESPONSE: 304\n\n" if $verbose;
  180. print $q->header( -status => '304 Not Modified', );
  181. return;
  182. }
  183. # what properties do we need?
  184. my $reqinfo;
  185. my @reqprops;
  186. $reqinfo = $req->find('/*/*')->shift->localname;
  187. if ($reqinfo eq 'prop') {
  188. for my $node ($req->find('/*/*/*')->get_nodelist) {
  189. push @reqprops, [ $node->namespaceURI, $node->localname ];
  190. }
  191. }
  192. # warn "reqprops: " . join(", ", map {join "", @$_} @reqprops) . "\n";
  193. # collection only, all pages, or single page?
  194. my @pages = OddMuse::AllPagesList();
  195. if ($q->path_info =~ '^/dav/?$') {
  196. # warn "collection!\n";
  197. if ($depth eq "0") {
  198. # warn "only the collection!\n";
  199. @pages = ('');
  200. } else {
  201. # warn "all pages!\n";
  202. unshift(@pages, '');
  203. }
  204. } else {
  205. my $id = OddMuse::GetId();
  206. # warn "single page, id: $id\n";
  207. if (not $OddMuse::IndexHash{$id}) {
  208. warn "RESPONSE: 404\n\n" if $verbose;
  209. print $q->header( -status => "404 Not Found", );
  210. print OddMuse::NewText($id);
  211. return;
  212. }
  213. @pages = ($id);
  214. }
  215. print $q->header( -status => "207 Multi-Status",
  216. -etag => md5_base64($OddMuse::LastUpdate
  217. . $req->toString)
  218. );
  219. my $doc = XML::LibXML::Document->new('1.0', 'utf-8');
  220. my $multistat = $doc->createElement('D:multistatus');
  221. $multistat->setAttribute('xmlns:D', 'DAV:');
  222. $doc->setDocumentElement($multistat);
  223. my %data = propfind_data();
  224. for my $id (@pages) {
  225. my $title = $id;
  226. $title =~ s/_/ /g;
  227. my ($size, $mtime, $ctime) = ('', '', ''); # undefined for the wiki proper ($id eq '')
  228. ($size, $mtime, $ctime) = @{$data{$id}} if $id;
  229. my $etag = $mtime; # $mtime is $Page{ts} which is used as etag in GET
  230. # modified time is stringified human readable HTTP::Date style
  231. $mtime = time2str($mtime);
  232. # created time is ISO format
  233. # tidy up date format - isoz isn't exactly what we want, but
  234. # it's easy to change.
  235. $ctime = time2isoz($ctime);
  236. $ctime =~ s/ /T/;
  237. $ctime =~ s/Z//;
  238. # force empty strings if undefined
  239. $size ||= '';
  240. my $resp = $doc->createElement('D:response');
  241. $multistat->addChild($resp);
  242. my $href = $doc->createElement('D:href');
  243. $href->appendText($OddMuse::ScriptName . '/dav/' . OddMuse::UrlEncode($id));
  244. $resp->addChild($href);
  245. my $okprops = $doc->createElement('D:prop');
  246. my $nfprops = $doc->createElement('D:prop');
  247. my $prop;
  248. if ($reqinfo eq 'prop') {
  249. my %prefixes = ('DAV:' => 'D');
  250. my $i = 0;
  251. for my $reqprop (@reqprops) {
  252. my ($ns, $name) = @$reqprop;
  253. if ($ns eq 'DAV:' && $name eq 'creationdate') {
  254. $prop = $doc->createElement('D:creationdate');
  255. $prop->appendText($ctime);
  256. $okprops->addChild($prop);
  257. } elsif ($ns eq 'DAV:' && $name eq 'getcontentlength') {
  258. $prop = $doc->createElement('D:getcontentlength');
  259. $prop->appendText($size);
  260. $okprops->addChild($prop);
  261. } elsif ($ns eq 'DAV:' && $name eq 'getcontenttype') {
  262. $prop = $doc->createElement('D:getcontenttype');
  263. $prop->appendText('text/plain');
  264. $okprops->addChild($prop);
  265. } elsif ($ns eq 'DAV:' && $name eq 'getlastmodified') {
  266. $prop = $doc->createElement('D:getlastmodified');
  267. $prop->appendText($mtime);
  268. $okprops->addChild($prop);
  269. } elsif ($ns eq 'DAV:' && $name eq 'resourcetype') {
  270. $prop = $doc->createElement('D:resourcetype');
  271. if (not $id) { # change for namespaces later
  272. my $col = $doc->createElement('D:collection');
  273. $prop->addChild($col);
  274. }
  275. $okprops->addChild($prop);
  276. } elsif ($ns eq 'DAV:' && $name eq 'displayname') {
  277. $prop = $doc->createElement('D:displayname');
  278. $prop->appendText($title);
  279. $okprops->addChild($prop);
  280. } elsif ($ns eq 'DAV:' && $name eq 'getetag') {
  281. $prop = $doc->createElement('D:getetag');
  282. $prop->appendText($etag);
  283. $okprops->addChild($prop);
  284. } else {
  285. my $prefix = $prefixes{$ns};
  286. if (!defined $prefix) {
  287. $prefix = 'i' . $i++;
  288. # mod_dav sets <response> 'xmlns' attribute - whatever
  289. #$nfprops->setAttribute("xmlns:$prefix", $ns);
  290. $resp->setAttribute("xmlns:$prefix", $ns);
  291. $prefixes{$ns} = $prefix;
  292. }
  293. $prop = $doc->createElement("$prefix:$name");
  294. $nfprops->addChild($prop);
  295. }
  296. }
  297. } elsif ($reqinfo eq 'propname') {
  298. $prop = $doc->createElement('D:creationdate');
  299. $okprops->addChild($prop);
  300. $prop = $doc->createElement('D:getcontentlength');
  301. $okprops->addChild($prop);
  302. $prop = $doc->createElement('D:getcontenttype');
  303. $okprops->addChild($prop);
  304. $prop = $doc->createElement('D:getlastmodified');
  305. $okprops->addChild($prop);
  306. $prop = $doc->createElement('D:resourcetype');
  307. $okprops->addChild($prop);
  308. $prop = $doc->createElement('D:displayname');
  309. $okprops->addChild($prop);
  310. $prop = $doc->createElement('D:getetag');
  311. $okprops->addChild($prop);
  312. } else {
  313. $prop = $doc->createElement('D:creationdate');
  314. $prop->appendText($ctime);
  315. $okprops->addChild($prop);
  316. $prop = $doc->createElement('D:getcontentlength');
  317. $prop->appendText($size);
  318. $okprops->addChild($prop);
  319. $prop = $doc->createElement('D:getcontenttype');
  320. $prop->appendText('text/plain');
  321. $okprops->addChild($prop);
  322. $prop = $doc->createElement('D:getlastmodified');
  323. $prop->appendText($mtime);
  324. $okprops->addChild($prop);
  325. $prop = $doc->createElement('D:resourcetype');
  326. if (not $id) { # change for namespaces later
  327. my $col = $doc->createElement('D:collection');
  328. $prop->addChild($col);
  329. }
  330. $okprops->addChild($prop);
  331. $prop = $doc->createElement('D:displayname');
  332. $prop->appendText($title);
  333. $okprops->addChild($prop);
  334. $prop = $doc->createElement('D:getetag');
  335. $prop->appendText($etag);
  336. $okprops->addChild($prop);
  337. }
  338. if ($okprops->hasChildNodes) {
  339. my $propstat = $doc->createElement('D:propstat');
  340. $propstat->addChild($okprops);
  341. my $stat = $doc->createElement('D:status');
  342. $stat->appendText('HTTP/1.1 200 OK');
  343. $propstat->addChild($stat);
  344. $resp->addChild($propstat);
  345. }
  346. if ($nfprops->hasChildNodes) {
  347. my $propstat = $doc->createElement('D:propstat');
  348. $propstat->addChild($nfprops);
  349. my $stat = $doc->createElement('D:status');
  350. $stat->appendText('HTTP/1.1 404 Not Found');
  351. $propstat->addChild($stat);
  352. $resp->addChild($propstat);
  353. }
  354. }
  355. warn "RESPONSE: 207\n" . $doc->toString(1) . "\n" if $verbose;
  356. print $doc->toString(1);
  357. }
  358. sub propfind_data {
  359. my %data = ();
  360. my $update = Modified($OddMuse::WebDavCache);
  361. if ($update and $OddMuse::LastUpdate == $update) {
  362. my $data = OddMuse::ReadFileOrDie($OddMuse::WebDavCache);
  363. map {
  364. my ($id, @attr) = split(/$OddMuse::FS/, $_);
  365. $data{$id} = \@attr;
  366. } split(/\n/, $data);
  367. } else {
  368. my @pages = OddMuse::AllPagesList();
  369. my $cache = '';
  370. foreach my $id (@pages) {
  371. OddMuse::OpenPage($id);
  372. my ($size, $mtime, $ctime);
  373. $size = length($OddMuse::Page{text}||0);
  374. $mtime = $OddMuse::Page{ts}||0;
  375. $ctime = $OddMuse::Page{created}||0;
  376. $data{$id} = [$size, $mtime, $ctime];
  377. $cache .= join($OddMuse::FS, $id, $size, $mtime, $ctime) . "\n";
  378. }
  379. if (OddMuse::RequestLockDir('webdav')) { # not fatal
  380. OddMuse::WriteStringToFile($OddMuse::WebDavCache, $cache);
  381. utime $OddMuse::LastUpdate, $OddMuse::LastUpdate, $OddMuse::WebDavCache; # touch index file
  382. OddMuse::ReleaseLockDir('webdav');
  383. }
  384. }
  385. return %data;
  386. }