summaryrefslogtreecommitdiff
path: root/IkiWiki/Plugin/attachment.pm
blob: 44781165cdaee4d6a0928d4dfe9ec5acc58cb066 (plain)
  1. #!/usr/bin/perl
  2. package IkiWiki::Plugin::attachment;
  3. use warnings;
  4. use strict;
  5. use IkiWiki 2.00;
  6. sub import { #{{{
  7. add_underlay("javascript");
  8. hook(type => "getsetup", id => "attachment", call => \&getsetup);
  9. hook(type => "checkconfig", id => "attachment", call => \&checkconfig);
  10. hook(type => "formbuilder_setup", id => "attachment", call => \&formbuilder_setup);
  11. hook(type => "formbuilder", id => "attachment", call => \&formbuilder);
  12. IkiWiki::loadplugin("filecheck");
  13. } # }}}
  14. sub getsetup () { #{{{
  15. return
  16. plugin => {
  17. safe => 1,
  18. rebuild => 0,
  19. },
  20. allowed_attachments => {
  21. type => "pagespec",
  22. example => "virusfree() and mimetype(image/*) and maxsize(50kb)",
  23. description => "enhanced PageSpec specifying what attachments are allowed",
  24. link => "ikiwiki/PageSpec/attachment",
  25. safe => 1,
  26. rebuild => 0,
  27. },
  28. virus_checker => {
  29. type => "string",
  30. example => "clamdscan -",
  31. description => "virus checker program (reads STDIN, returns nonzero if virus found)",
  32. safe => 0, # executed
  33. rebuild => 0,
  34. },
  35. } #}}}
  36. sub check_canattach ($$;$) { #{{{
  37. my $session=shift;
  38. my $dest=shift; # where it's going to be put, under the srcdir
  39. my $file=shift; # the path to the attachment currently
  40. # Don't allow an attachment to be uploaded with the same name as an
  41. # existing page.
  42. if (exists $IkiWiki::pagesources{$dest} &&
  43. $IkiWiki::pagesources{$dest} ne $dest) {
  44. error(sprintf(gettext("there is already a page named %s"), $dest));
  45. }
  46. # Use a special pagespec to test that the attachment is valid.
  47. my $allowed=1;
  48. if (defined $config{allowed_attachments} &&
  49. length $config{allowed_attachments}) {
  50. $allowed=pagespec_match($dest,
  51. $config{allowed_attachments},
  52. file => $file,
  53. user => $session->param("name"),
  54. ip => $ENV{REMOTE_ADDR},
  55. );
  56. }
  57. # XXX deprecated, should be removed eventually
  58. if ($allowed) {
  59. foreach my $admin (@{$config{adminuser}}) {
  60. my $allowed_attachments=IkiWiki::userinfo_get($admin, "allowed_attachments");
  61. if (defined $allowed_attachments &&
  62. length $allowed_attachments) {
  63. $allowed=pagespec_match($dest,
  64. $allowed_attachments,
  65. file => $file,
  66. user => $session->param("name"),
  67. ip => $ENV{REMOTE_ADDR},
  68. );
  69. last if $allowed;
  70. }
  71. }
  72. }
  73. if (! $allowed) {
  74. error(gettext("prohibited by allowed_attachments")." ($allowed)");
  75. }
  76. else {
  77. return 1;
  78. }
  79. } #}}}
  80. sub checkconfig () { #{{{
  81. $config{cgi_disable_uploads}=0;
  82. } #}}}
  83. sub formbuilder_setup (@) { #{{{
  84. my %params=@_;
  85. my $form=$params{form};
  86. my $q=$params{cgi};
  87. if (defined $form->field("do") && ($form->field("do") eq "edit" ||
  88. $form->field("do") eq "create")) {
  89. # Add attachment field, set type to multipart.
  90. $form->enctype(&CGI::MULTIPART);
  91. $form->field(name => 'attachment', type => 'file');
  92. # These buttons are not put in the usual place, so
  93. # are not added to the normal formbuilder button list.
  94. $form->tmpl_param("field-upload" => '<input name="_submit" type="submit" value="Upload Attachment" />');
  95. $form->tmpl_param("field-link" => '<input name="_submit" type="submit" value="Insert Links" />');
  96. # Add the toggle javascript; the attachments interface uses
  97. # it to toggle visibility.
  98. require IkiWiki::Plugin::toggle;
  99. $form->tmpl_param("javascript" => IkiWiki::Plugin::toggle::include_javascript($params{page}, 1));
  100. # Start with the attachments interface toggled invisible,
  101. # but if it was used, keep it open.
  102. if ($form->submitted ne "Upload Attachment" &&
  103. (! defined $q->param("attachment_select") ||
  104. ! length $q->param("attachment_select"))) {
  105. $form->tmpl_param("attachments-class" => "toggleable");
  106. }
  107. else {
  108. $form->tmpl_param("attachments-class" => "toggleable-open");
  109. }
  110. }
  111. elsif ($form->title eq "preferences") {
  112. # XXX deprecated, should remove eventually
  113. my $session=$params{session};
  114. my $user_name=$session->param("name");
  115. $form->field(name => "allowed_attachments", size => 50,
  116. fieldset => "admin",
  117. comment => "deprecated; please move to allowed_attachments in setup file",
  118. );
  119. if (! IkiWiki::is_admin($user_name)) {
  120. $form->field(name => "allowed_attachments", type => "hidden");
  121. }
  122. if (! $form->submitted) {
  123. my $value=IkiWiki::userinfo_get($user_name, "allowed_attachments");
  124. if (length $value) {
  125. $form->field(name => "allowed_attachments", force => 1,
  126. value => IkiWiki::userinfo_get($user_name, "allowed_attachments"));
  127. }
  128. else {
  129. $form->field(name => "allowed_attachments", type => "hidden");
  130. }
  131. }
  132. if ($form->submitted && $form->submitted eq 'Save Preferences') {
  133. if (defined $form->field("allowed_attachments")) {
  134. IkiWiki::userinfo_set($user_name, "allowed_attachments",
  135. $form->field("allowed_attachments")) ||
  136. error("failed to set allowed_attachments");
  137. if (! length $form->field("allowed_attachments")) {
  138. $form->field(name => "allowed_attachments", type => "hidden");
  139. }
  140. }
  141. }
  142. }
  143. } #}}}
  144. sub formbuilder (@) { #{{{
  145. my %params=@_;
  146. my $form=$params{form};
  147. my $q=$params{cgi};
  148. return if ! defined $form->field("do") || ($form->field("do") ne "edit" && $form->field("do") ne "create") ;
  149. my $filename=$q->param('attachment');
  150. if (defined $filename && length $filename &&
  151. ($form->submitted eq "Upload Attachment" || $form->submitted eq "Save Page")) {
  152. my $session=$params{session};
  153. # This is an (apparently undocumented) way to get the name
  154. # of the temp file that CGI writes the upload to.
  155. my $tempfile=$q->tmpFileName($filename);
  156. if (! defined $tempfile || ! length $tempfile) {
  157. # perl 5.8 needs an alternative, awful method
  158. if ($q =~ /HASH/ && exists $q->{'.tmpfiles'}) {
  159. foreach my $key (keys(%{$q->{'.tmpfiles'}})) {
  160. $tempfile=$q->tmpFileName(\$key);
  161. last if defined $tempfile && length $tempfile;
  162. }
  163. }
  164. if (! defined $tempfile || ! length $tempfile) {
  165. error("CGI::tmpFileName failed to return the uploaded file name");
  166. }
  167. }
  168. $filename=linkpage(IkiWiki::possibly_foolish_untaint(
  169. attachment_location($form->field('page')).
  170. IkiWiki::basename($filename)));
  171. if (IkiWiki::file_pruned($filename, $config{srcdir})) {
  172. error(gettext("bad attachment filename"));
  173. }
  174. # Check that the user is allowed to edit a page with the
  175. # name of the attachment.
  176. IkiWiki::check_canedit($filename, $q, $session, 1);
  177. # And that the attachment itself is acceptable.
  178. check_canattach($session, $filename, $tempfile);
  179. # Needed for fast_file_copy and for rendering below.
  180. require IkiWiki::Render;
  181. # Move the attachment into place.
  182. # Try to use a fast rename; fall back to copying.
  183. IkiWiki::prep_writefile($filename, $config{srcdir});
  184. unlink($config{srcdir}."/".$filename);
  185. if (rename($tempfile, $config{srcdir}."/".$filename)) {
  186. # The temp file has tight permissions; loosen up.
  187. chmod(0666 & ~umask, $config{srcdir}."/".$filename);
  188. }
  189. else {
  190. my $fh=$q->upload('attachment');
  191. if (! defined $fh || ! ref $fh) {
  192. # needed by old CGI versions
  193. $fh=$q->param('attachment');
  194. if (! defined $fh || ! ref $fh) {
  195. # even that doesn't always work,
  196. # fall back to opening the tempfile
  197. $fh=undef;
  198. open($fh, "<", $tempfile) || error("failed to open \"$tempfile\": $!");
  199. }
  200. }
  201. binmode($fh);
  202. writefile($filename, $config{srcdir}, undef, 1, sub {
  203. IkiWiki::fast_file_copy($tempfile, $filename, $fh, @_);
  204. });
  205. }
  206. # Check the attachment in and trigger a wiki refresh.
  207. if ($config{rcs}) {
  208. IkiWiki::rcs_add($filename);
  209. IkiWiki::disable_commit_hook();
  210. IkiWiki::rcs_commit($filename, gettext("attachment upload"),
  211. IkiWiki::rcs_prepedit($filename),
  212. $session->param("name"), $ENV{REMOTE_ADDR});
  213. IkiWiki::enable_commit_hook();
  214. IkiWiki::rcs_update();
  215. }
  216. IkiWiki::refresh();
  217. IkiWiki::saveindex();
  218. }
  219. elsif ($form->submitted eq "Insert Links") {
  220. my $page=quotemeta($q->param("page"));
  221. my $add="";
  222. foreach my $f ($q->param("attachment_select")) {
  223. $f=~s/^$page\///;
  224. $add.="[[$f]]\n";
  225. }
  226. $form->field(name => 'editcontent',
  227. value => $form->field('editcontent')."\n\n".$add,
  228. force => 1) if length $add;
  229. }
  230. # Generate the attachment list only after having added any new
  231. # attachments.
  232. $form->tmpl_param("attachment_list" => [attachment_list($form->field('page'))]);
  233. } # }}}
  234. sub attachment_location ($) { #{{{
  235. my $page=shift;
  236. # Put the attachment in a subdir of the page it's attached
  237. # to, unless that page is an "index" page.
  238. $page=~s/(^|\/)index//;
  239. $page.="/" if length $page;
  240. return $page;
  241. } #}}}
  242. sub attachment_list ($) { #{{{
  243. my $page=shift;
  244. my $loc=attachment_location($page);
  245. my @ret;
  246. foreach my $f (values %pagesources) {
  247. if (! defined pagetype($f) &&
  248. $f=~m/^\Q$loc\E[^\/]+$/ &&
  249. -e "$config{srcdir}/$f") {
  250. push @ret, {
  251. "field-select" => '<input type="checkbox" name="attachment_select" value="'.$f.'" />',
  252. link => htmllink($page, $page, $f, noimageinline => 1),
  253. size => IkiWiki::Plugin::filecheck::humansize((stat(_))[7]),
  254. mtime => displaytime($IkiWiki::pagemtime{$f}),
  255. };
  256. }
  257. }
  258. # Sort newer attachments to the top of the list, so a newly-added
  259. # attachment appears just before the form used to add it.
  260. return sort { $b->{mtime_raw} <=> $a->{mtime_raw} || $a->{link} cmp $b->{link} } @ret;
  261. } #}}}
  262. 1