summaryrefslogtreecommitdiff
path: root/IkiWiki/Plugin/editpage.pm
blob: bb21ed2be486adce4de82dab3076b10a5f8e99ad (plain)
  1. #!/usr/bin/perl
  2. package IkiWiki::Plugin::editpage;
  3. use warnings;
  4. use strict;
  5. use IkiWiki;
  6. use open qw{:utf8 :std};
  7. sub import { #{{{
  8. hook(type => "getsetup", id => "editpage", call => \&getsetup);
  9. hook(type => "sessioncgi", id => "editpage", call => \&IkiWiki::cgi_editpage);
  10. } # }}}
  11. sub getsetup () { #{{{
  12. return
  13. plugin => {
  14. safe => 1,
  15. rebuild => 1,
  16. },
  17. } #}}}
  18. # Back to ikiwiki namespace for the rest, this code is very much
  19. # internal to ikiwiki even though it's separated into a plugin,
  20. # and other plugins use the functions below.
  21. package IkiWiki;
  22. sub check_canedit ($$$;$) { #{{{
  23. my $page=shift;
  24. my $q=shift;
  25. my $session=shift;
  26. my $nonfatal=shift;
  27. my $canedit;
  28. run_hooks(canedit => sub {
  29. return if defined $canedit;
  30. my $ret=shift->($page, $q, $session);
  31. if (defined $ret) {
  32. if ($ret eq "") {
  33. $canedit=1;
  34. }
  35. elsif (ref $ret eq 'CODE') {
  36. $ret->() unless $nonfatal;
  37. $canedit=0;
  38. }
  39. elsif (defined $ret) {
  40. error($ret) unless $nonfatal;
  41. $canedit=0;
  42. }
  43. }
  44. });
  45. return $canedit;
  46. } #}}}
  47. sub cgi_editpage ($$) { #{{{
  48. my $q=shift;
  49. my $session=shift;
  50. my $do=$q->param('do');
  51. return unless $do eq 'create' || $do eq 'edit';
  52. decode_cgi_utf8($q);
  53. my @fields=qw(do rcsinfo subpage from page type editcontent comments);
  54. my @buttons=("Save Page", "Preview", "Cancel");
  55. eval q{use CGI::FormBuilder};
  56. error($@) if $@;
  57. my $form = CGI::FormBuilder->new(
  58. fields => \@fields,
  59. charset => "utf-8",
  60. method => 'POST',
  61. required => [qw{editcontent}],
  62. javascript => 0,
  63. params => $q,
  64. action => $config{cgiurl},
  65. header => 0,
  66. table => 0,
  67. template => scalar template_params("editpage.tmpl"),
  68. wikiname => $config{wikiname},
  69. );
  70. decode_form_utf8($form);
  71. run_hooks(formbuilder_setup => sub {
  72. shift->(form => $form, cgi => $q, session => $session,
  73. buttons => \@buttons);
  74. });
  75. decode_form_utf8($form);
  76. # This untaint is safe because we check file_pruned.
  77. my $page=$form->field('page');
  78. $page=possibly_foolish_untaint($page);
  79. my $absolute=($page =~ s#^/+##);
  80. if (! defined $page || ! length $page ||
  81. file_pruned($page, $config{srcdir})) {
  82. error("bad page name");
  83. }
  84. my $baseurl=$config{url}."/".htmlpage($page);
  85. my $from;
  86. if (defined $form->field('from')) {
  87. ($from)=$form->field('from')=~/$config{wiki_file_regexp}/;
  88. }
  89. my $file;
  90. my $type;
  91. if (exists $pagesources{$page} && $form->field("do") ne "create") {
  92. $file=$pagesources{$page};
  93. $type=pagetype($file);
  94. if (! defined $type || $type=~/^_/) {
  95. error(sprintf(gettext("%s is not an editable page"), $page));
  96. }
  97. if (! $form->submitted) {
  98. $form->field(name => "rcsinfo",
  99. value => rcs_prepedit($file), force => 1);
  100. }
  101. $form->field(name => "editcontent", validate => '/.*/');
  102. }
  103. else {
  104. $type=$form->param('type');
  105. if (defined $type && length $type && $hooks{htmlize}{$type}) {
  106. $type=possibly_foolish_untaint($type);
  107. }
  108. elsif (defined $from && exists $pagesources{$from}) {
  109. # favor the type of linking page
  110. $type=pagetype($pagesources{$from});
  111. }
  112. $type=$config{default_pageext} unless defined $type;
  113. $file=$page.".".$type;
  114. if (! $form->submitted) {
  115. $form->field(name => "rcsinfo", value => "", force => 1);
  116. }
  117. $form->field(name => "editcontent", validate => '/.+/');
  118. }
  119. $form->field(name => "do", type => 'hidden');
  120. $form->field(name => "sid", type => "hidden", value => $session->id,
  121. force => 1);
  122. $form->field(name => "from", type => 'hidden');
  123. $form->field(name => "rcsinfo", type => 'hidden');
  124. $form->field(name => "subpage", type => 'hidden');
  125. $form->field(name => "page", value => $page, force => 1);
  126. $form->field(name => "type", value => $type, force => 1);
  127. $form->field(name => "comments", type => "text", size => 80);
  128. $form->field(name => "editcontent", type => "textarea", rows => 20,
  129. cols => 80);
  130. $form->tmpl_param("can_commit", $config{rcs});
  131. $form->tmpl_param("indexlink", indexlink());
  132. $form->tmpl_param("helponformattinglink",
  133. htmllink($page, $page, "ikiwiki/formatting",
  134. noimageinline => 1,
  135. linktext => "FormattingHelp"));
  136. if ($form->submitted eq "Cancel") {
  137. if ($form->field("do") eq "create" && defined $from) {
  138. redirect($q, "$config{url}/".htmlpage($from));
  139. }
  140. elsif ($form->field("do") eq "create") {
  141. redirect($q, $config{url});
  142. }
  143. else {
  144. redirect($q, "$config{url}/".htmlpage($page));
  145. }
  146. exit;
  147. }
  148. elsif ($form->submitted eq "Preview") {
  149. my $new=not exists $pagesources{$page};
  150. if ($new) {
  151. # temporarily record its type
  152. $pagesources{$page}=$page.".".$type;
  153. }
  154. my $content=$form->field('editcontent');
  155. run_hooks(editcontent => sub {
  156. $content=shift->(
  157. content => $content,
  158. page => $page,
  159. cgi => $q,
  160. session => $session,
  161. );
  162. });
  163. my $preview=htmlize($page, $page, $type,
  164. linkify($page, $page,
  165. preprocess($page, $page,
  166. filter($page, $page, $content), 0, 1)));
  167. run_hooks(format => sub {
  168. $preview=shift->(
  169. page => $page,
  170. content => $preview,
  171. );
  172. });
  173. $form->tmpl_param("page_preview", $preview);
  174. if ($new) {
  175. delete $pagesources{$page};
  176. }
  177. # previewing may have created files on disk
  178. saveindex();
  179. }
  180. elsif ($form->submitted eq "Save Page") {
  181. $form->tmpl_param("page_preview", "");
  182. }
  183. if ($form->submitted ne "Save Page" || ! $form->validate) {
  184. if ($form->field("do") eq "create") {
  185. my @page_locs;
  186. my $best_loc;
  187. if (! defined $from || ! length $from ||
  188. $from ne $form->field('from') ||
  189. file_pruned($from, $config{srcdir}) ||
  190. $from=~/^\// ||
  191. $absolute ||
  192. $form->submitted eq "Preview") {
  193. @page_locs=$best_loc=$page;
  194. }
  195. else {
  196. my $dir=$from."/";
  197. $dir=~s![^/]+/+$!!;
  198. if ((defined $form->field('subpage') && length $form->field('subpage')) ||
  199. $page eq gettext('discussion')) {
  200. $best_loc="$from/$page";
  201. }
  202. else {
  203. $best_loc=$dir.$page;
  204. }
  205. push @page_locs, $dir.$page;
  206. push @page_locs, "$from/$page";
  207. while (length $dir) {
  208. $dir=~s![^/]+/+$!!;
  209. push @page_locs, $dir.$page;
  210. }
  211. push @page_locs, "$config{userdir}/$page"
  212. if length $config{userdir};
  213. }
  214. @page_locs = grep {
  215. ! exists $pagecase{lc $_}
  216. } @page_locs;
  217. if (! @page_locs) {
  218. # hmm, someone else made the page in the
  219. # meantime?
  220. if ($form->submitted eq "Preview") {
  221. # let them go ahead with the edit
  222. # and resolve the conflict at save
  223. # time
  224. @page_locs=$page;
  225. }
  226. else {
  227. redirect($q, "$config{url}/".htmlpage($page));
  228. exit;
  229. }
  230. }
  231. my @editable_locs = grep {
  232. check_canedit($_, $q, $session, 1)
  233. } @page_locs;
  234. if (! @editable_locs) {
  235. # let it throw an error this time
  236. map { check_canedit($_, $q, $session) } @page_locs;
  237. }
  238. my @page_types;
  239. if (exists $hooks{htmlize}) {
  240. @page_types=grep { !/^_/ }
  241. keys %{$hooks{htmlize}};
  242. }
  243. $form->tmpl_param("page_select", 1);
  244. $form->field(name => "page", type => 'select',
  245. options => [ map { [ $_, pagetitle($_, 1) ] } @editable_locs ],
  246. value => $best_loc);
  247. $form->field(name => "type", type => 'select',
  248. options => \@page_types);
  249. $form->title(sprintf(gettext("creating %s"), pagetitle($page)));
  250. }
  251. elsif ($form->field("do") eq "edit") {
  252. check_canedit($page, $q, $session);
  253. if (! defined $form->field('editcontent') ||
  254. ! length $form->field('editcontent')) {
  255. my $content="";
  256. if (exists $pagesources{$page}) {
  257. $content=readfile(srcfile($pagesources{$page}));
  258. $content=~s/\n/\r\n/g;
  259. }
  260. $form->field(name => "editcontent", value => $content,
  261. force => 1);
  262. }
  263. $form->tmpl_param("page_select", 0);
  264. $form->field(name => "page", type => 'hidden');
  265. $form->field(name => "type", type => 'hidden');
  266. $form->title(sprintf(gettext("editing %s"), pagetitle($page)));
  267. }
  268. showform($form, \@buttons, $session, $q, forcebaseurl => $baseurl);
  269. }
  270. else {
  271. # save page
  272. check_canedit($page, $q, $session);
  273. # The session id is stored on the form and checked to
  274. # guard against CSRF. But only if the user is logged in,
  275. # as anonok can allow anonymous edits.
  276. if (defined $session->param("name")) {
  277. my $sid=$q->param('sid');
  278. if (! defined $sid || $sid ne $session->id) {
  279. error(gettext("Your login session has expired."));
  280. }
  281. }
  282. my $exists=-e "$config{srcdir}/$file";
  283. if ($form->field("do") ne "create" && ! $exists &&
  284. ! defined srcfile($file, 1)) {
  285. $form->tmpl_param("message", template("editpagegone.tmpl")->output);
  286. $form->field(name => "do", value => "create", force => 1);
  287. $form->tmpl_param("page_select", 0);
  288. $form->field(name => "page", type => 'hidden');
  289. $form->field(name => "type", type => 'hidden');
  290. $form->title(sprintf(gettext("editing %s"), $page));
  291. showform($form, \@buttons, $session, $q, forcebaseurl => $baseurl);
  292. exit;
  293. }
  294. elsif ($form->field("do") eq "create" && $exists) {
  295. $form->tmpl_param("message", template("editcreationconflict.tmpl")->output);
  296. $form->field(name => "do", value => "edit", force => 1);
  297. $form->tmpl_param("page_select", 0);
  298. $form->field(name => "page", type => 'hidden');
  299. $form->field(name => "type", type => 'hidden');
  300. $form->title(sprintf(gettext("editing %s"), $page));
  301. $form->field("editcontent",
  302. value => readfile("$config{srcdir}/$file").
  303. "\n\n\n".$form->field("editcontent"),
  304. force => 1);
  305. showform($form, \@buttons, $session, $q, forcebaseurl => $baseurl);
  306. exit;
  307. }
  308. my $content=$form->field('editcontent');
  309. run_hooks(editcontent => sub {
  310. $content=shift->(
  311. content => $content,
  312. page => $page,
  313. cgi => $q,
  314. session => $session,
  315. );
  316. });
  317. $content=~s/\r\n/\n/g;
  318. $content=~s/\r/\n/g;
  319. $content.="\n" if $content !~ /\n$/;
  320. $config{cgi}=0; # avoid cgi error message
  321. eval { writefile($file, $config{srcdir}, $content) };
  322. $config{cgi}=1;
  323. if ($@) {
  324. $form->field(name => "rcsinfo", value => rcs_prepedit($file),
  325. force => 1);
  326. my $mtemplate=template("editfailedsave.tmpl");
  327. $mtemplate->param(error_message => $@);
  328. $form->tmpl_param("message", $mtemplate->output);
  329. $form->field("editcontent", value => $content, force => 1);
  330. $form->tmpl_param("page_select", 0);
  331. $form->field(name => "page", type => 'hidden');
  332. $form->field(name => "type", type => 'hidden');
  333. $form->title(sprintf(gettext("editing %s"), $page));
  334. showform($form, \@buttons, $session, $q,
  335. forcebaseurl => $baseurl);
  336. exit;
  337. }
  338. my $conflict;
  339. if ($config{rcs}) {
  340. my $message="";
  341. if (defined $form->field('comments') &&
  342. length $form->field('comments')) {
  343. $message=$form->field('comments');
  344. }
  345. if (! $exists) {
  346. rcs_add($file);
  347. }
  348. # Prevent deadlock with post-commit hook by
  349. # signaling to it that it should not try to
  350. # do anything.
  351. disable_commit_hook();
  352. $conflict=rcs_commit($file, $message,
  353. $form->field("rcsinfo"),
  354. $session->param("name"), $ENV{REMOTE_ADDR});
  355. enable_commit_hook();
  356. rcs_update();
  357. }
  358. # Refresh even if there was a conflict, since other changes
  359. # may have been committed while the post-commit hook was
  360. # disabled.
  361. require IkiWiki::Render;
  362. refresh();
  363. saveindex();
  364. if (defined $conflict) {
  365. $form->field(name => "rcsinfo", value => rcs_prepedit($file),
  366. force => 1);
  367. $form->tmpl_param("message", template("editconflict.tmpl")->output);
  368. $form->field("editcontent", value => $conflict, force => 1);
  369. $form->field("do", "edit", force => 1);
  370. $form->tmpl_param("page_select", 0);
  371. $form->field(name => "page", type => 'hidden');
  372. $form->field(name => "type", type => 'hidden');
  373. $form->title(sprintf(gettext("editing %s"), $page));
  374. showform($form, \@buttons, $session, $q,
  375. forcebaseurl => $baseurl);
  376. }
  377. else {
  378. # The trailing question mark tries to avoid broken
  379. # caches and get the most recent version of the page.
  380. redirect($q, "$config{url}/".htmlpage($page)."?updated");
  381. }
  382. }
  383. exit;
  384. } #}}}
  385. 1