static-hybrid.pl 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. # Copyright (C) 2005 Fletcher T. Penney <fletcher@freeshell.org>
  2. # Copyright (C) 2004 Alex Schroeder <alex@emacswiki.org>
  3. #
  4. # This program is free software; you can redistribute it and/or modify
  5. # it under the terms of the GNU General Public License as published by
  6. # the Free Software Foundation; either version 2 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU General Public License
  15. # along with this program; if not, write to the
  16. # Free Software Foundation, Inc.
  17. # 59 Temple Place, Suite 330
  18. # Boston, MA 02111-1307 USA
  19. use strict;
  20. use v5.10;
  21. AddModuleDescription('static-hybrid.pl', 'Static Hybrid Module');
  22. our ($q, $Now, %Action, %Page, %IndexHash, @IndexList, $OpenPageName, $ScriptName, $FS, $RCName, $DeletedPage, $UsePathInfo, $CommentsPrefix, $Message, $KeepDays, $EmbedWiki, $ClusterMapPage, %NearLinksUsed);
  23. our ($StaticDir, $StaticAlways, %StaticMimeTypes, $StaticUrl,
  24. %StaticLinkedPages, @StaticIgnoredPages);
  25. $Action{static} = \&DoStatic;
  26. $StaticDir = '' unless defined $StaticDir;
  27. $StaticUrl = '' unless defined $StaticUrl; # change this!
  28. $StaticAlways = 0 unless defined $StaticAlways;
  29. # 1 = uploaded files only, 2 = all pages
  30. my $StaticMimeTypes = '/etc/http/mime.types'; # all-ASCII characters
  31. my %StaticFiles;
  32. my $StaticAction = 0; # Are we doing action or not?
  33. my @StaticQueue = ();
  34. my $ClusterHasChanged = 0;
  35. my $PageBeingSaved = "";
  36. sub DoStatic {
  37. $StaticAction = 1;
  38. return unless UserIsAdminOrError();
  39. my $raw = GetParam('raw', 0);
  40. if ($raw) {
  41. print GetHttpHeader('text/plain');
  42. } else {
  43. print GetHeader('', T('Static Copy'), '');
  44. }
  45. CreateDir($StaticDir);
  46. %StaticMimeTypes = StaticMimeTypes() unless %StaticMimeTypes;
  47. %StaticFiles = ();
  48. my $id = GetParam('id', '');
  49. if ($id) {
  50. local *GetDownloadLink = \&StaticGetDownloadLink;
  51. StaticWriteFile($id);
  52. } else {
  53. StaticWriteFiles();
  54. }
  55. print '</p>' unless $raw;
  56. PrintFooter() unless $raw;
  57. }
  58. sub StaticMimeTypes {
  59. my %hash;
  60. # the default mapping matches the default @UploadTypes...
  61. open(my $F, '<', $StaticMimeTypes)
  62. or return ('image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif');
  63. while (<$F>) {
  64. s/\#.*//; # remove comments
  65. my($type, $ext) = split;
  66. $hash{$type} = $ext if $ext;
  67. }
  68. close($F);
  69. return %hash;
  70. }
  71. sub StaticWriteFiles {
  72. my $raw = GetParam('raw', 0);
  73. local *GetDownloadLink = \&StaticGetDownloadLink;
  74. foreach my $id (AllPagesList()) {
  75. SetParam('rcclusteronly',0);
  76. if (! grep(/^$id$/,@StaticIgnoredPages)) {
  77. StaticWriteFile($id);
  78. }
  79. }
  80. }
  81. sub StaticGetDownloadLink {
  82. my ($name, $image, $revision, $alt) = @_; # ignore $revision
  83. $alt = $name unless $alt;
  84. my $id = FreeToNormal($name);
  85. AllPagesList();
  86. # if the page does not exist
  87. return '[' . ($image ? 'image' : 'link') . ':' . $name . ']' unless $IndexHash{$id};
  88. if ($image) {
  89. my $result = $q->img({-src=>StaticFileName($id), -alt=>$alt, -class=>'upload'});
  90. $result = ScriptLink($id, $result, 'image');
  91. return $result;
  92. } else {
  93. return ScriptLink($id, $alt, 'upload');
  94. }
  95. }
  96. sub StaticFileName {
  97. my $id = shift;
  98. $id =~ s/ /_/g;
  99. $id =~ s/#.*//; # remove named anchors for the filename test
  100. return $StaticFiles{$id} if $StaticFiles{$id}; # cache filenames
  101. my ($status, $data) = ReadFile(GetPageFile(StaticUrlDecode($id)));
  102. print "cannot read " . GetPageFile(StaticUrlDecode($id)) . $q->br() unless $status;
  103. my $hash = ParseData($data);
  104. my $ext = '.html';
  105. if ($hash->{text} =~ /^\#FILE ([^ \n]+)\n(.*)/s) {
  106. $ext = $StaticMimeTypes{$1};
  107. $ext = '.' . $ext if $ext;
  108. }
  109. $StaticFiles{$id} = $id . $ext;
  110. return $StaticFiles{$id};
  111. }
  112. sub StaticUrlDecode {
  113. my $str = shift;
  114. $str =~ s/%([0-9a-f][0-9a-f])/chr(hex($1))/eg;
  115. return $str;
  116. }
  117. sub StaticWriteFile {
  118. my $id = shift;
  119. my $raw = GetParam('raw', 0);
  120. my $html = GetParam('html', 1);
  121. OpenPage($id);
  122. my ($mimetype, $data) = $Page{text} =~ /^\#FILE ([^ \n]+)\n(.*)/s;
  123. return unless $html or $data;
  124. my $filename = StaticFileName($id);
  125. open(my $F, '>', encode_utf8("$StaticDir/$filename")) or ReportError(Ts('Cannot write %s', $filename));
  126. if ($data) {
  127. StaticFile($id, $mimetype, $data, $F);
  128. } elsif ($html) {
  129. StaticHtml($id, $F);
  130. }
  131. close($F);
  132. chmod 0644,"$StaticDir/$filename";
  133. if (lc(GetParam('action','')) eq "static") {
  134. print $filename, $raw ? "\n" : $q->br();
  135. }
  136. }
  137. sub StaticFile {
  138. my ($id, $type, $data, $F) = @_;
  139. require MIME::Base64;
  140. binmode($F);
  141. print $F MIME::Base64::decode($data);
  142. }
  143. sub StaticHtml {
  144. my $id = FreeToNormal(shift);
  145. my $F = shift;
  146. my $title = $id;
  147. $title =~ s/_/ /g;
  148. local *GetHttpHeader = \&StaticGetHttpHeader;
  149. local *GetCommentForm = \&StaticGetCommentForm;
  150. %NearLinksUsed = ();
  151. # Isolate our output
  152. local *STDERR;
  153. open(STDERR, '>', '/dev/null');
  154. # Process the page
  155. local $Message = "";
  156. # encoding is left off, so fix it:
  157. my $result = ToString(sub {
  158. print qq!<?xml version="1.0" encoding="UTF-8" ?>!;
  159. print GetHeader($id, QuoteHtml($id), undef, "");
  160. print $q->start_div({-class=> 'content browse'});
  161. print PageHtml($id);
  162. print $q->end_div();
  163. SetParam('rcclusteronly', $id) if (FreeToNormal(GetCluster($Page{text})) eq $id);
  164. if (($id eq $RCName) || (T($RCName) eq $id) || (T($id) eq $RCName)
  165. || GetParam('rcclusteronly', '')) {
  166. print $q->start_div({-class=>'rc'});;
  167. print $q->hr() if not GetParam('embed', $EmbedWiki);
  168. DoRc(\&GetRcHtml);
  169. print $q->end_div();
  170. }
  171. PrintFooter($id);
  172. });
  173. print $F $result;
  174. return;
  175. }
  176. *StaticFilesOldDoPost = \&DoPost;
  177. *DoPost = \&StaticFilesNewDoPost;
  178. sub StaticFilesNewDoPost {
  179. my $id = FreeToNormal(shift);
  180. OpenPage($id);
  181. my $old_cluster = FreeToNormal(GetCluster($Page{text}));
  182. StaticFilesOldDoPost($id);
  183. my $new_cluster = FreeToNormal(GetCluster($Page{text}));
  184. $ClusterHasChanged = 1 if ($old_cluster ne $new_cluster);
  185. if ($StaticAlways) {
  186. # always delete
  187. StaticDeleteFile($OpenPageName);
  188. if ($Page{text} =~ /^\#FILE / # if a file was uploaded
  189. or $StaticAlways > 1) {
  190. CreateDir($StaticDir);
  191. # If new Page added, update index
  192. if (! $IndexHash{$OpenPageName} ) {
  193. push(@IndexList, $OpenPageName);
  194. $IndexHash{$OpenPageName} = 1;
  195. }
  196. StaticWriteFile($OpenPageName);
  197. $PageBeingSaved = $OpenPageName;
  198. AddLinkedFilesToQueue($OpenPageName);
  199. StaticWriteLinkedFiles();
  200. }
  201. }
  202. }
  203. *StaticOldDeletePage = \&DeletePage;
  204. *DeletePage = \&StaticNewDeletePage;
  205. sub StaticNewDeletePage {
  206. my $id = shift;
  207. StaticDeleteFile($id) if ($StaticAlways);
  208. return StaticOldDeletePage($id);
  209. }
  210. sub StaticDeleteFile {
  211. my $id = shift;
  212. %StaticMimeTypes = StaticMimeTypes() unless %StaticMimeTypes;
  213. # we don't care if the files or $StaticDir don't exist -- just delete!
  214. for my $f (map { "$StaticDir/$id.$_" } (values %StaticMimeTypes, 'html')) {
  215. Unlink($f); # delete copies with different extensions
  216. }
  217. }
  218. # override the default!
  219. sub GetDownloadLink {
  220. my ($name, $image, $revision, $alt) = @_;
  221. $alt = $name unless $alt;
  222. my $id = FreeToNormal($name);
  223. AllPagesList();
  224. # if the page does not exist
  225. return '[' . ($image ? T('image') : T('download')) . ':' . $name
  226. . ']' . GetEditLink($id, '?', 1) unless $IndexHash{$id};
  227. my $action;
  228. if ($revision) {
  229. $action = "action=download;id=" . UrlEncode($id) . ";revision=$revision";
  230. } elsif ($UsePathInfo) {
  231. $action = "download/" . UrlEncode($id);
  232. } else {
  233. $action = "action=download;id=" . UrlEncode($id);
  234. }
  235. if ($image) {
  236. if ($UsePathInfo and not $revision) {
  237. if ($StaticAlways and $StaticUrl) {
  238. my $url = $StaticUrl;
  239. my $img = UrlEncode(StaticFileName($id));
  240. $url =~ s/\%s/$img/g or $url .= $img;
  241. $action = $url;
  242. } else {
  243. $action = $ScriptName . '/' . $action;
  244. }
  245. } else {
  246. $action = $ScriptName . '?' . $action;
  247. }
  248. my $result = $q->img({-src=>$action, -alt=>$alt, -class=>'upload'});
  249. $result = ScriptLink(UrlEncode($id), $result, 'image') unless $id eq $OpenPageName;
  250. return $result;
  251. } else {
  252. return ScriptLink($action, $alt, 'upload');
  253. }
  254. }
  255. # override function from Image Extension to support advanced image tags
  256. sub ImageGetInternalUrl{
  257. my $id = shift;
  258. if ($UsePathInfo) {
  259. if ($StaticAlways and $StaticUrl) {
  260. my $url = $StaticUrl;
  261. my $img = UrlEncode(StaticFileName($id));
  262. $url =~ s/\%s/$img/g or $url .= $img;
  263. return $url;
  264. } else {
  265. return $ScriptName . '/download/' . UrlEncode($id);
  266. }
  267. }
  268. return $ScriptName . '?action=download;id=' . UrlEncode($id);
  269. }
  270. sub AddLinkedFilesToQueue {
  271. my $id = shift;
  272. foreach my $pattern (keys %StaticLinkedPages) {
  273. if ($id =~ /$pattern/) {
  274. AddNewFilesToQueue(@{$StaticLinkedPages{$pattern}})
  275. }
  276. }
  277. # If you modify a comment page, then update the original
  278. # Don't check for recursive updates - the only thing that
  279. # changed was the CommentCount - no reason to waste time
  280. if ($id =~ /^$CommentsPrefix(.*)/) {
  281. my $match = $1;
  282. push(@StaticQueue,$match);
  283. }
  284. # If the page added belongs to a cluster, update the cluster's page
  285. # and the $ClusterMapPage
  286. # especially important with the clustermap module
  287. local %Page;
  288. local $OpenPageName = '';
  289. OpenPage($id);
  290. my $cluster = FreeToNormal(GetCluster($Page{text}));
  291. # Only move up the cluster hierarchy if the page we originally
  292. # edited has a cluster
  293. if ($PageBeingSaved = $id) {
  294. if ($cluster ne "" && $cluster ne $id) {
  295. AddNewFilesToQueue($cluster);
  296. # If we are using clustermaps then update
  297. # ClusterMapPage
  298. # But only if cluster has changed
  299. if ($ClusterHasChanged) {
  300. if ($ClusterMapPage ne "") {
  301. AddNewFilesToQueue($ClusterMapPage);
  302. }
  303. }
  304. }
  305. }
  306. }
  307. sub StaticWriteLinkedFiles {
  308. my $raw = GetParam('raw', 0);
  309. my $writeRC = 0;
  310. local *GetDownloadLink = \&StaticGetDownloadLink;
  311. foreach my $id (@StaticQueue) {
  312. if (! grep(/^$id$/,@StaticIgnoredPages)) {
  313. StaticWriteFile($id);
  314. SetParam('rcclusteronly',0);
  315. }
  316. }
  317. }
  318. sub StaticGetCommentForm {
  319. my ($id, $rev, $comment) = @_;
  320. if ($CommentsPrefix ne '' and $id and $rev ne 'history' and $rev ne 'edit'
  321. and $OpenPageName =~ /^$CommentsPrefix/) {
  322. return $q->div({-class=>'comment'}, GetFormStart(undef, undef, 'comment'),
  323. $q->p(GetHiddenValue('title', $OpenPageName),
  324. GetTextArea('aftertext', $comment)),
  325. $q->p(T('Username:'), ' ',
  326. $q->textfield(-name=>'username', -default=>'',
  327. -override=>1, -size=>20, -maxlength=>50),
  328. T('Homepage URL:'), ' ',
  329. $q->textfield(-name=>'homepage', -default=>'',
  330. -override=>1, -size=>40, -maxlength=>100)),
  331. $q->p($q->submit(-name=>'Save', -accesskey=>T('s'), -value=>T('Save')), ' ',
  332. $q->submit(-name=>'Preview', -value=>T('Preview'))),
  333. $q->end_form());
  334. }
  335. return '';
  336. }
  337. sub StaticGetHttpHeader {
  338. return;
  339. }
  340. sub AddNewFilesToQueue {
  341. # Add a file to queue, but only if not already there
  342. my @ids = @_;
  343. foreach my $id (@ids) {
  344. if (! grep(/^$id$/,@StaticQueue)) {
  345. push(@StaticQueue,$id);
  346. AddLinkedFilesToQueue($id);
  347. }
  348. }
  349. }
  350. # Make rollback compatible
  351. *StaticOldDoRollback = \&DoRollback;
  352. *DoRollback = \&StaticNewDoRollback;
  353. $Action{rollback} = \&StaticNewDoRollback;
  354. # Delete the static file so that changes made during a rollback are propogated
  355. sub StaticNewDoRollback {
  356. my $page = shift;
  357. my $to = GetParam('to', 0);
  358. ReportError(T('Missing target for rollback.'), '400 BAD REQUEST') unless $to;
  359. ReportError(T('Target for rollback is too far back.'), '400 BAD REQUEST') unless $page or RollbackPossible($to);
  360. ReportError(T('A username is required for ordinary users.'), '403 FORBIDDEN') unless GetParam('username', '') or UserIsEditor();
  361. my @ids = ();
  362. if (not $page) { # cannot just use list length because of ('')
  363. return unless UserIsAdminOrError(); # only admins can do mass changes
  364. my %ids = map { my ($ts, $id) = split(/$FS/); $id => 1; } # make unique via hash
  365. GetRcLines($Now - $KeepDays * 86400, 1); # 24*60*60
  366. @ids = keys %ids;
  367. } else {
  368. @ids = ($page);
  369. }
  370. RequestLockOrError();
  371. print GetHeader('', T('Rolling back changes')), $q->start_div({-class=>'content rollback'}), $q->start_p();
  372. foreach my $id (@ids) {
  373. OpenPage($id);
  374. my ($text, $minor, $ts) = GetTextAtTime($to);
  375. if ($Page{text} eq $text) {
  376. print T("The two revisions are the same."), $q->br() if $page; # no message when doing mass revert
  377. } elsif (!UserCanEdit($id, 1)) {
  378. print Ts('Editing not allowed for %s.', $id), $q->br();
  379. } else {
  380. Save($id, $text, Ts('Rollback to %s', TimeToText($to)), $minor, ($Page{host} ne $q->remote_addr()));
  381. StaticDeleteFile($id);
  382. print Ts('%s rolled back', GetPageLink($id)), ($ts ? ' ' . Ts('to %s', TimeToText($to)) : ''), $q->br();
  383. }
  384. }
  385. WriteRcLog('[[rollback]]', '', $to) unless $page; # leave marker for DoRc() if mass rollback
  386. print $q->end_p() . $q->end_div();
  387. ReleaseLock();
  388. PrintFooter();
  389. }
  390. *StaticOldDespamPage = \&DespamPage;
  391. *DespamPage = \&StaticNewDespamPage;
  392. sub StaticNewDespamPage {
  393. my $rule = shift;
  394. # from DoHistory()
  395. my @revisions = sort {$b <=> $a} map { m|/([0-9]+).kp$|; $1; } GetKeepFiles($OpenPageName);
  396. foreach my $revision (@revisions) {
  397. my ($revisionPage, $rev) = GetTextRevision($revision, 1); # quiet
  398. if (not $rev) {
  399. print ': ' . Ts('Cannot find revision %s.', $revision);
  400. return;
  401. } elsif (not DespamBannedContent($revisionPage->{text})) {
  402. my $summary = Tss('Revert to revision %1: %2', $revision, $rule);
  403. print ': ' . $summary;
  404. Save($OpenPageName, $revisionPage->{text}, $summary) unless GetParam('debug', 0);
  405. StaticDeleteFile($OpenPageName);
  406. return;
  407. }
  408. }
  409. if (grep(/^1$/, @revisions) or not @revisions) { # if there is no kept revision, yet
  410. my $summary = Ts($rule). ' ' . Ts('Marked as %s.', $DeletedPage);
  411. print ': ' . $summary;
  412. Save($OpenPageName, $DeletedPage, $summary) unless GetParam('debug', 0);
  413. StaticDeleteFile($OpenPageName);
  414. } else {
  415. print ': ' . T('Cannot find unspammed revision.');
  416. }
  417. }