static-copy.pl 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. # Copyright (C) 2004-2014 Alex Schroeder <alex@gnu.org>
  2. #
  3. # This program is free software: you can redistribute it and/or modify it under
  4. # the terms of the GNU General Public License as published by the Free Software
  5. # Foundation, either version 3 of the License, or (at your option) any later
  6. # version.
  7. #
  8. # This program is distributed in the hope that it will be useful, but WITHOUT
  9. # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  10. # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
  11. #
  12. # You should have received a copy of the GNU General Public License along with
  13. # this program. If not, see <http://www.gnu.org/licenses/>.
  14. use strict;
  15. use v5.10;
  16. AddModuleDescription('static-copy.pl', 'Static Copy Extension');
  17. our ($q, %Page, %IndexHash, $OpenPageName, $ScriptName, $SiteName, $UsePathInfo, %Action, $CommentsPrefix, $FreeLinks, $WikiLinks, $LinkPattern, $FreeLinkPattern, $StyleSheet, $StyleSheetPage, $TopLinkBar, $UserGotoBar, $LogoUrl, $SidebarName);
  18. $Action{static} = \&DoStatic;
  19. our ($StaticDir, $StaticAlways, %StaticMimeTypes, $StaticUrl);
  20. $StaticDir = '/tmp/static';
  21. $StaticUrl = ''; # change this!
  22. $StaticAlways = 0; # 1 = uploaded files only, 2 = all pages
  23. my $StaticMimeTypes = '/etc/mime.types';
  24. my %StaticFiles;
  25. sub DoStatic {
  26. return unless UserIsAdminOrError();
  27. my $raw = GetParam('raw', 0);
  28. if ($raw) {
  29. print GetHttpHeader('text/plain');
  30. } else {
  31. print GetHeader('', T('Static Copy'), '');
  32. }
  33. CreateDir($StaticDir);
  34. %StaticFiles = ();
  35. print '<p>' unless $raw;
  36. StaticWriteFiles(1);
  37. print '</p>' unless $raw;
  38. PrintFooter() unless $raw;
  39. }
  40. sub StaticMimeTypes {
  41. my %hash;
  42. # the default mapping matches the default @UploadTypes...
  43. open(my $fh, '<', $StaticMimeTypes)
  44. or return ('image/jpeg' => 'jpg', 'image/png' => 'png', );
  45. while (<$fh>) {
  46. s/\#.*//; # remove comments
  47. my($type, $ext) = split;
  48. $hash{$type} = $ext if $ext;
  49. }
  50. close($fh);
  51. return %hash;
  52. }
  53. sub StaticWriteFiles {
  54. my $verbose = shift;
  55. my $raw = GetParam('raw', 0);
  56. my $html = GetParam('html', 0);
  57. local *ScriptLink = \&StaticScriptLink;
  58. local *GetDownloadLink = \&StaticGetDownloadLink;
  59. foreach my $id (AllPagesList()) {
  60. if ($StaticAlways > 1
  61. or $html
  62. or PageIsUploadedFile($id)) {
  63. StaticWriteFile($id, $html, $verbose);
  64. }
  65. }
  66. if ($StaticAlways > 1 or $html) {
  67. StaticWriteCss();
  68. }
  69. }
  70. sub StaticScriptLink {
  71. my ($action, $text, $class, $name, $title, $accesskey) = @_;
  72. my %params;
  73. if ($action !~ /=/) {
  74. # the page might not exist, eg. if called via GetAuthorLink
  75. $params{'-href'} = StaticFileName($action) if $IndexHash{UrlDecode($action)};
  76. }
  77. $params{'-class'} = $class if $class;
  78. $params{'-name'} = UrlEncode($name) if $name;
  79. $params{'-title'} = $title if $title;
  80. $params{'-accesskey'} = $accesskey if $accesskey;
  81. return $q->a(\%params, $text);
  82. }
  83. sub StaticGetDownloadLink {
  84. my ($name, $image, $revision, $alt) = @_; # ignore $revision
  85. $alt = $name unless $alt;
  86. $alt =~ s/_/ /g;
  87. my $id = FreeToNormal($name);
  88. # if the page does not exist
  89. return '[' . ($image ? 'image' : 'link') . ':' . $name . ']' unless $IndexHash{$id};
  90. if ($image) {
  91. return StaticFileName($id) if $image == 2;
  92. my $result = $q->img({-src=>StaticFileName($id), -alt=>$alt, -class=>'upload', -loading=>'lazy'});
  93. $result = ScriptLink($id, $result, 'image');
  94. return $result;
  95. } else {
  96. return ScriptLink($id, $alt, 'upload');
  97. }
  98. }
  99. sub StaticFileName {
  100. my $id = shift;
  101. $id =~ s/#.*//; # remove named anchors for the filename test
  102. return $StaticFiles{$id} if $StaticFiles{$id}; # cache filenames
  103. # Don't clober current open page so don't use OpenPage. UrlDecode
  104. # the $id to open the file because when called from
  105. # StaticScriptLink, for example, the $action is already encoded.
  106. my ($status, $data) = ReadFile(GetPageFile(UrlDecode($id)));
  107. # If the link points to a wanted page, we cannot make this static.
  108. return $id unless $status;
  109. my $hash = ParseData($data);
  110. my $ext = '.html';
  111. if ($hash->{text} =~ /^\#FILE ([^ \n]+ ?[^ \n]*)\n(.*)/s) {
  112. %StaticMimeTypes = StaticMimeTypes() unless %StaticMimeTypes;
  113. $ext = $StaticMimeTypes{"$1"};
  114. $ext = '.' . $ext if $ext;
  115. }
  116. $StaticFiles{$id} = $id . $ext;
  117. return $StaticFiles{$id};
  118. }
  119. sub StaticWriteFile {
  120. my ($id, $html, $verbose) = @_;
  121. my $raw = GetParam('raw', 0);
  122. OpenPage($id);
  123. my ($mimetype, $encoding, $data) =
  124. $Page{text} =~ /^\#FILE ([^ \n]+) ?([^ \n]*)\n(.*)/s;
  125. my $filename = StaticFileName($id);
  126. my $file = "$StaticDir/$filename";
  127. if ($data) {
  128. open(my $fh, '>', encode_utf8($file))
  129. or ReportError(Ts('Cannot write %s', $filename));
  130. StaticFile($id, $fh, $mimetype, $data);
  131. close($fh);
  132. } elsif ($html) {
  133. open(my $fh, '>:encoding(UTF-8)', encode_utf8($file))
  134. or ReportError(Ts('Cannot write %s', $filename));
  135. StaticHtml($id, $fh);
  136. close($fh);
  137. } else {
  138. print "no data for " if $verbose;
  139. }
  140. ChangeMod(0644,"$StaticDir/$filename");
  141. print $filename, $raw ? "\n" : $q->br() if $verbose;
  142. }
  143. sub StaticFile {
  144. my ($id, $fh, $type, $data) = @_;
  145. require MIME::Base64;
  146. print $fh (MIME::Base64::decode($data));
  147. }
  148. sub StaticHtml {
  149. my ($id, $fh) = @_; # assume open page
  150. # redirect
  151. if (($FreeLinks and $Page{text} =~ /^\#REDIRECT\s+\[\[$FreeLinkPattern\]\]/)
  152. or ($WikiLinks and $Page{text} =~ /^\#REDIRECT\s+$LinkPattern/)) {
  153. my $target = StaticFileName($1);
  154. print $fh <<"EOT";
  155. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  156. <html>
  157. <head>
  158. <title>$SiteName: $id</title>
  159. <link type="text/css" rel="stylesheet" href="static.css" />
  160. <meta http-equiv="refresh" content="0; url=$target">
  161. <meta http-equiv="content-type" content="text/html; charset=UTF-8">
  162. </head>
  163. <body>
  164. <p>Redirected to <a href="$target">$1</a>.</p>
  165. </body>
  166. </html>
  167. EOT
  168. return;
  169. }
  170. print $fh <<"EOT";
  171. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
  172. <html>
  173. <head>
  174. <title>$SiteName: $id</title>
  175. <link type="text/css" rel="stylesheet" href="static.css" />
  176. <meta http-equiv="content-type" content="text/html; charset=UTF-8">
  177. </head>
  178. <body>
  179. EOT
  180. my $header = '';
  181. # logo
  182. if ($LogoUrl) {
  183. my $logo = $LogoUrl;
  184. $logo =~ s|.*/||; # just the filename
  185. my $alt = T('[Home]');
  186. $header .= $q->img({-src=>$logo, -alt=>$alt, -class=>'logo', -loading=>'lazy'}) if $logo;
  187. }
  188. # top toolbar
  189. local $UserGotoBar = ''; # only allow @UserGotoBarPages
  190. my $toolbar = GetGotoBar($id);
  191. $header .= $toolbar if GetParam('toplinkbar', $TopLinkBar);
  192. # title
  193. my $name = $id;
  194. $name =~ s|_| |g;
  195. $header .= $q->h1($name);
  196. print $fh $q->div({-class=>'header'}, $header);
  197. # sidebar, if the module is loaded
  198. print $fh $q->div({-class=>'sidebar'}, PageHtml($SidebarName)) if $SidebarName;
  199. # content
  200. print $fh $q->div({-class=>'content'}, PageHtml($id)); # this reopens the page currently open
  201. # footer
  202. my $links = '';
  203. if ($OpenPageName !~ /^$CommentsPrefix/ # fails if $CommentsPrefix is empty!
  204. and $IndexHash{$CommentsPrefix . $OpenPageName}) { # TODO use $CommentsPattern
  205. $links .= ScriptLink(UrlEncode($CommentsPrefix . $OpenPageName),
  206. T('Comments on this page'));
  207. }
  208. if ($CommentsPrefix and $id =~ /^$CommentsPrefix(.*)/) {
  209. $links .= ' | ' if $links;
  210. $links .= Ts('Back to %s', GetPageLink($1, $1));
  211. }
  212. $links = $q->br() . $links if $links;
  213. print $fh $q->div({-class=>'footer'}, $q->hr(), $toolbar,
  214. $q->span({-class=>'edit'}, $links),
  215. $q->span({-class=>'time'}, GetFooterTimestamp($id)));
  216. # finish
  217. print $fh '</body></html>';
  218. }
  219. sub StaticWriteCss {
  220. my $css;
  221. if ($StyleSheet) {
  222. if (ref $StyleSheet) {
  223. $css = join '', map { GetRaw($_) } @$StyleSheet;
  224. } else {
  225. $css = GetRaw($StyleSheet);
  226. }
  227. }
  228. if (not $css and $IndexHash{$StyleSheetPage}) {
  229. $css = GetPageContent($StyleSheetPage);
  230. }
  231. if (not $css) {
  232. $css = GetRaw('https://oddmuse.org/default.css');
  233. }
  234. WriteStringToFile("$StaticDir/static.css", $css) if $css;
  235. chmod 0644,"$StaticDir/static.css";
  236. }
  237. *StaticFilesOldSave = \&Save;
  238. *Save = \&StaticFilesNewSave;
  239. sub StaticFilesNewSave {
  240. my ($id, $new) = @_;
  241. StaticFilesOldSave(@_);
  242. if ($StaticAlways) {
  243. # always delete
  244. StaticDeleteFile($id);
  245. if ($new =~ /^\#FILE / # if a file was uploaded
  246. or $StaticAlways > 1) {
  247. CreateDir($StaticDir);
  248. StaticWriteFile($OpenPageName);
  249. }
  250. }
  251. }
  252. *StaticOldDeletePage = \&DeletePage;
  253. *DeletePage = \&StaticNewDeletePage;
  254. sub StaticNewDeletePage {
  255. my $id = shift;
  256. StaticDeleteFile($id) if ($StaticAlways);
  257. return StaticOldDeletePage($id);
  258. }
  259. sub StaticDeleteFile {
  260. my $id = shift;
  261. %StaticMimeTypes = StaticMimeTypes() unless %StaticMimeTypes;
  262. # we don't care if the files or $StaticDir don't exist -- just delete!
  263. for my $f (map { "$StaticDir/$id.$_" } (values %StaticMimeTypes, 'html')) {
  264. Unlink($f); # delete copies with different extensions
  265. }
  266. }
  267. # override the default!
  268. sub GetDownloadLink {
  269. my ($name, $image, $revision, $alt) = @_;
  270. $alt = $name unless $alt;
  271. my $id = FreeToNormal($name);
  272. AllPagesList();
  273. # if the page does not exist
  274. return '[' . ($image ? T('image') : T('download')) . ':' . $name
  275. . ']' . GetEditLink($id, '?', 1) unless $IndexHash{$id};
  276. my $action;
  277. if ($revision) {
  278. $action = "action=download;id=" . UrlEncode($id) . ";revision=$revision";
  279. } elsif ($UsePathInfo) {
  280. $action = "download/" . UrlEncode($id);
  281. } else {
  282. $action = "action=download;id=" . UrlEncode($id);
  283. }
  284. if ($image) {
  285. if ($UsePathInfo and not $revision) {
  286. if ($StaticAlways and $StaticUrl) {
  287. my $url = $StaticUrl;
  288. my $img = UrlEncode(StaticFileName($id));
  289. $url =~ s/\%s/$img/g or $url .= $img;
  290. $action = $url;
  291. } else {
  292. $action = $ScriptName . '/' . $action;
  293. }
  294. } else {
  295. $action = $ScriptName . '?' . $action;
  296. }
  297. return $action if $image == 2;
  298. my $result = $q->img({-src=>$action, -alt=>$alt, -class=>'upload', -loading=>'lazy'});
  299. $result = ScriptLink(UrlEncode($id), $result, 'image') unless $id eq $OpenPageName;
  300. return $result;
  301. } else {
  302. return ScriptLink($action, $alt, 'upload');
  303. }
  304. }
  305. # override function from Image Extension to support advanced image tags
  306. sub ImageGetInternalUrl {
  307. my $id = FreeToNormal(shift);
  308. if ($UsePathInfo) {
  309. if ($StaticAlways and $StaticUrl) {
  310. my $url = $StaticUrl;
  311. my $img = UrlEncode(StaticFileName($id));
  312. $url =~ s/\%s/$img/g or $url .= $img;
  313. return $url;
  314. } else {
  315. return $ScriptName . '/download/' . UrlEncode($id);
  316. }
  317. }
  318. return $ScriptName . '?action=download;id=' . UrlEncode($id);
  319. }