two more cases
[ikiwiki] / doc / todo / Moving_Pages.mdwn
1 I thought I'd draw attention to a desire of mine for **ikiwiki**.  I'm no power-user, and mostly I do fairly simple stuff with my [wiki](http://kitenet.net/~kyle/family/wiki).
2
3 However, I would like the ability (now) to **rename/move/delete** pages.  As part of having a genealogy wiki, I've put name and dates of birth/death as part of the title of each article (so to avoid cases where people have the same name, but are children/cousins/etc of others with that name).  However, some of this information changes.  For instance, I didn't know a date of death and now I do, or I had it wrong originally, or it turns out someone is still alive I didn't know about.  All of these cases leave me with bad article titles.
4
5 So, I can go ahead and move the file to a new page with the correct info, orphan that page, provide a link for the new page if desired, and otherwise ignore that page.  But then, it clutters up the wiki and serves no useful purpose.
6
7 Anyway to consider implementing **rename/move/delete** ?  I certainly lack the skills to appreciate what this would entail, but feel free to comment if it appears impossible, and then I'll go back to the aforementioned workaround.  I would prefer simple rename, however.
8
9 Thanks again to [Joey](http://kitenet.net/~joey) for putting ikiwiki together.  I love the program. 
10
11 *[Kyle](http://kitenet.net/~kyle/)=*
12
13 ----
14
15 The MediaWiki moving/renaming mechanism is pretty nice.  It's easy to get a list of pages that point to the current page.  When renaming a page it sticks a forwarding page in the original place.  The larger the size of the wiki the more important organization tools become.
16
17 I see the need for:
18
19 * a new type of file to represent a forwarding page
20 * a rename tool that can
21   * move the existing page to the new name
22   * optionally drop a forwarding page
23   * optionally rewrite incoming links to the new location
24
25 Brad
26
27 > This could be implemented through the use of an HTTP redirect to the 
28 > new page, but this has the downside that people may not know they're being 
29 > redirected.
30 >
31 > This could also be implemented using a combination of raw inline and meta
32 > to change the title (add a "redirected from etc." page. This could be done
33 > with a plugin. A redirect page would be [[redirect page="newpage"]].
34 > But then if you click "edit" on this redirect page, you won't be able
35 > to edit the new page, only the call to redirect.
36 > --Ethan
37
38 -----
39
40 [[tag patch]]
41
42 This is my second cut at a feature like that requested here.
43 It can also be found [here](http://ikidev.betacantrips.com/patches/move.patch).
44
45 A few shortcomings exist: 
46
47 * No precautions whatsoever are made to protect against race conditions or failures
48   in the rcs\_move function. I didn't even do the `cgi_editpage` thing where I hold
49   the lock and render afterwards (mostly because the copy I was editing was not
50   up-to-date enough to have that code). Although FAILED_SAVE is in movepage.tmpl,
51   no code activates it yet.
52 * Some code is duplicated between cgi\_movepage and cgi\_editpage, as well
53   as rcs\_commit and rcs\_move.
54 * The user interface is pretty lame. I couldn't figure out a good way to let
55   the user specify which directory to move things to without implementing a
56   FileChooser thing.
57 * No redirect pages like those mentioned on [[todo/Moving_Pages]] exist yet, 
58   so none are created.
59 * I added a Move link to page.tmpl but it may belong better someplace else --
60   maybe editpage.tmpl? Not sure.
61 * from is redundant with page so far -- but since the Move links could someday
62   come from someplace other than the page itself I kept it around.
63 * If I move foo.mdwn to bar.mdwn, foo/* should move too, probably.
64
65 > Looks like a good start, although I agree about many of the points above,
66 > and also feel that something needs to be done about rcses that don't
67 > implement a move operation -- falling back to an add and delete.
68 > --[[Joey]]
69
70 Hmm. Shouldn't that be done on a by-RCS basis, though? (i.e. implemented
71 by backends in the `rcs_move` function)
72
73 > Probably, yes, but maybe there's a way to avoid duplicating code for that
74 > in several of them.
75
76 Also, how should ikiwiki react if a page is edited (say, by another user)
77 before it is moved? Bail, or shrug and proceed?
78
79 > The important thing is to keep in mind that the page could be edited,
80 > moved, deleted, etc in between the user starting the move and the move
81 > happening. So, the code really needs to deal with all of these cases in
82 > some way. It seems fine to me to go ahead with the move even if the page
83 > was edited. If the page was deleted or moved, it seems reasonable to exit
84 > with an error.
85
86     diff -urNX ignorepats ikiwiki/IkiWiki/CGI.pm ikidev/IkiWiki/CGI.pm
87     --- ikiwiki/IkiWiki/CGI.pm  2007-02-14 18:17:12.000000000 -0800
88     +++ ikidev/IkiWiki/CGI.pm   2007-02-22 18:54:23.194982000 -0800
89     @@ -561,6 +561,106 @@
90         }
91      } #}}}
92
93     +sub cgi_movepage($$) {
94     +   my $q = shift;
95     +   my $session = shift;
96     +   eval q{use CGI::FormBuilder};
97     +   error($@) if $@;
98     +   my @fields=qw(do from rcsinfo page newdir newname comments);
99     +   my @buttons=("Rename Page", "Cancel");
100     +
101     +   my $form = CGI::FormBuilder->new(
102     +           fields => \@fields,
103     +                header => 1,
104     +                charset => "utf-8",
105     +                method => 'POST',
106     +           action => $config{cgiurl},
107     +                template => (-e "$config{templatedir}/movepage.tmpl" ?
108     +                        {template_params("movepage.tmpl")} : ""),
109     +   );
110     +   run_hooks(formbuilder_setup => sub {
111     +           shift->(form => $form, cgi => $q, session => $session);
112     +   });
113     +
114     +   decode_form_utf8($form);
115     +
116     +   # This untaint is safe because if the page doesn't exist, bail.
117     +   my $page = $form->field('page');
118     +   $page = possibly_foolish_untaint($page);
119     +   if (! exists $pagesources{$page}) {
120     +           error("page does not exist");
121     +   }
122     +   my $file=$pagesources{$page};
123     +   my $type=pagetype($file);
124     +
125     +   my $from;
126     +   if (defined $form->field('from')) {
127     +           ($from)=$form->field('from')=~/$config{wiki_file_regexp}/;
128     +   }
129     +
130     +   $form->field(name => "do", type => 'hidden');
131     +   $form->field(name => "from", type => 'hidden');
132     +   $form->field(name => "rcsinfo", type => 'hidden');
133     +   $form->field(name => "newdir", type => 'text', size => 80);
134     +   $form->field(name => "page", value => $page, force => 1);
135     +   $form->field(name => "newname", type => "text", size => 80);
136     +   $form->field(name => "comments", type => "text", size => 80);
137     +   $form->tmpl_param("can_commit", $config{rcs});
138     +   $form->tmpl_param("indexlink", indexlink());
139     +   $form->tmpl_param("baseurl", baseurl());
140     +
141     +   if (! $form->submitted) {
142     +           $form->field(name => "rcsinfo", value => rcs_prepedit($file),
143     +                        force => 1);
144     +   }
145     +
146     +   if ($form->submitted eq "Cancel") {
147     +           redirect($q, "$config{url}/".htmlpage($page));
148     +           return;
149     +   }
150     +
151     +   if (! $form->submitted || ! $form->validate) {
152     +           check_canedit($page, $q, $session);
153     +           $form->tmpl_param("page_select", 0);
154     +           $form->field(name => "page", type => 'hidden');
155     +           $form->field(name => "type", type => 'hidden');
156     +           $form->title(sprintf(gettext("moving %s"), pagetitle($page)));
157     +           my $pname = basename($page);
158     +           my $dname = dirname($page);
159     +           if (! defined $form->field('newname') ||
160     +               ! length $form->field('newname')) {
161     +                   $form->field(name => "newname",
162     +                                value => pagetitle($pname, 1), force => 1);
163     +           }
164     +           if (! defined $form->field('newdir') ||
165     +               ! length $form->field('newdir')) {
166     +                   $form->field(name => "newdir",
167     +                                value => pagetitle($dname, 1), force => 1);
168     +           }
169     +           print $form->render(submit => \@buttons);
170     +   }
171     +   else{
172     +           # This untaint is safe because titlepage removes any problematic
173     +           # characters.
174     +           my ($newname)=$form->field('newname');
175     +           $newname=titlepage(possibly_foolish_untaint($newname));
176     +           my ($newdir)=$form->field('newdir');
177     +           $newdir=titlepage(possibly_foolish_untaint($newdir));
178     +           if (! defined $newname || ! length $newname || file_pruned($newname, $config{srcdir}) || $newname=~/^\//) {
179     +                   error("bad page name");
180     +           }
181     +           check_canedit($page, $q, $session);
182     +
183     +           my $newpage = ($newdir?"$newdir/":"") . $newname;
184     +           my $newfile = $newpage . ".$type";
185     +           my $message = $form->field('comments');
186     +           unlockwiki();
187     +           rcs_move($file, $newfile, $message, $form->field("rcsinfo"),
188     +                    $session->param("name"), $ENV{REMOTE_ADDR});
189     +           redirect($q, "$config{url}/".htmlpage($newpage));
190     +   }
191     +}
192     +
193      sub cgi_getsession ($) { #{{{
194         my $q=shift;
195
196     @@ -656,6 +756,9 @@
197         elsif (defined $session->param("postsignin")) {
198                 cgi_postsignin($q, $session);
199         }
200     +   elsif ($do eq 'move') {
201     +           cgi_movepage($q, $session);
202     +   }
203         elsif ($do eq 'prefs') {
204                 cgi_prefs($q, $session);
205         }
206     diff -urNX ignorepats ikiwiki/IkiWiki/Rcs/svn.pm ikidev/IkiWiki/Rcs/svn.pm
207     --- ikiwiki/IkiWiki/Rcs/svn.pm      2007-01-27 16:04:48.000000000 -0800
208     +++ ikidev/IkiWiki/Rcs/svn.pm       2007-02-22 01:51:29.923626000 -0800
209     @@ -60,6 +60,34 @@
210         }
211      } #}}}
212
213     +sub rcs_move ($$$$;$$) {
214     +   my $file=shift;
215     +   my $newname=shift;
216     +   my $message=shift;
217     +   my $rcstoken=shift;
218     +   my $user=shift;
219     +   my $ipaddr=shift;
220     +   if (defined $user) {
221     +           $message="web commit by $user".(length $message ? ": $message" : "");
222     +   }
223     +   elsif (defined $ipaddr) {
224     +           $message="web commit from $ipaddr".(length $message ? ": $message" : "");
225     +   }
226     +
227     +   chdir($config{srcdir}); # svn merge wants to be here
228     +
229     +   if (system("svn", "move", "--quiet",
230     +              "$file", "$newname") != 0) {
231     +           return 1;
232     +   }
233     +   if (system("svn", "commit", "--quiet",
234     +              "--encoding", "UTF-8", "-m",
235     +              possibly_foolish_untaint($message)) != 0) {
236     +           return 1;
237     +   }
238     +   return undef # success
239     +}
240     +
241      sub rcs_commit ($$$;$$) { #{{{
242         # Tries to commit the page; returns undef on _success_ and
243         # a version of the page with the rcs's conflict markers on failure.
244     diff -urNX ignorepats ikiwiki/IkiWiki/Render.pm ikidev/IkiWiki/Render.pm
245     --- ikiwiki/IkiWiki/Render.pm       2007-02-14 17:00:05.000000000 -0800
246     +++ ikidev/IkiWiki/Render.pm        2007-02-22 18:30:00.451755000 -0800
247     @@ -80,6 +80,7 @@
248
249         if (length $config{cgiurl}) {
250                 $template->param(editurl => cgiurl(do => "edit", page => $page));
251     +           $template->param(moveurl => cgiurl(do => "move", page => $page));
252                 $template->param(prefsurl => cgiurl(do => "prefs"));
253                 if ($config{rcs}) {
254                         $template->param(recentchangesurl => cgiurl(do => "recentchanges"));
255     diff -urNX ignorepats ikiwiki/templates/movepage.tmpl ikidev/templates/movepage.tmpl
256     --- ikiwiki/templates/movepage.tmpl 1969-12-31 16:00:00.000000000 -0800
257     +++ ikidev/templates/movepage.tmpl  2007-02-22 18:40:39.751763000 -0800
258     @@ -0,0 +1,44 @@
259     +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
260     + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
261     +<html>
262     +<head>
263     +<base href="<TMPL_VAR BASEURL>" />
264     +<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
265     +<title><TMPL_VAR FORM-TITLE></title>
266     +<link rel="stylesheet" href="<TMPL_VAR BASEURL>style.css" type="text/css" />
267     +<link rel="stylesheet" href="<TMPL_VAR BASEURL>local.css" type="text/css" />
268     +<TMPL_IF NAME="FAVICON">
269     +<link rel="icon" href="<TMPL_VAR BASEURL><TMPL_VAR FAVICON>" type="image/x-icon" />
270     +</TMPL_IF>
271     +</head>
272     +<body>
273     +<TMPL_IF NAME="FAILED_SAVE">
274     +<p>
275     +<b>Failed to save your changes.</b>
276     +</p>
277     +<p>
278     +Your changes were not able to be saved to disk. The system gave the error:
279     +<blockquote>
280     +<TMPL_VAR ERROR_MESSAGE>
281     +</blockquote>
282     +Your changes are preserved below, and you can try again to save them.
283     +</p>
284     +</TMPL_IF>
285     +<TMPL_VAR FORM-START>
286     +<div class="header">
287     +<span><TMPL_VAR INDEXLINK>/ <TMPL_VAR FORM-TITLE></span>
288     +</div>
289     +<TMPL_VAR FIELD-DO>
290     +<TMPL_VAR FIELD-FROM>
291     +<TMPL_VAR FIELD-RCSINFO>
292     +<TMPL_VAR FIELD-PAGE>
293     +New location: <TMPL_VAR FIELD-NEWDIR>/ <TMPL_VAR FIELD-NEWNAME>
294     +<br />
295     +<TMPL_IF NAME="CAN_COMMIT">
296     +Optional comment about this change:<br />
297     +<TMPL_VAR FIELD-COMMENTS><br />
298     +</TMPL_IF>
299     +<input id="_submit" name="_submit" type="submit" value="Rename Page" /><input id="_submit_2" name="_submit" type="submit" value="Cancel" />
300     +<TMPL_VAR FORM-END>
301     +</body>
302     +</html>
303     diff -urNX ignorepats ikiwiki/templates/page.tmpl ikidev/templates/page.tmpl
304     --- ikiwiki/templates/page.tmpl     2006-12-28 12:27:01.000000000 -0800
305     +++ ikidev/templates/page.tmpl      2007-02-22 01:52:33.078464000 -0800
306     @@ -32,6 +32,9 @@
307      <TMPL_IF NAME="EDITURL">
308      <li><a href="<TMPL_VAR EDITURL>">Edit</a></li>
309      </TMPL_IF>
310     +<TMPL_IF NAME="MOVEURL">
311     +<li><a href="<TMPL_VAR MOVEURL>">Move</a></li>
312     +</TMPL_IF>
313      <TMPL_IF NAME="RECENTCHANGESURL">
314      <li><a href="<TMPL_VAR RECENTCHANGESURL>">RecentChanges</a></li>
315      </TMPL_IF>
316
317 ----
318
319 I'm going to try to run through a full analysis and design for moving and
320 deleting pages here. I want to make sure all cases are covered. --[[Joey]]
321
322 ## UI
323
324 The UI I envision is to add "Rename" and "Delete" buttons to the file edit
325 page. Both next to the Save button, and also at the bottom of the attachment
326 management interface.
327
328 The attachment(s) to rename or delete would be selected using the check boxes
329 and then the button applies to all of them. Deleting multiple attachments
330 in one go is fine; renaming multiple attachments in one go is ambiguous,
331 and it can just error out if more than one is selected for rename.
332 (Alternatively, it could allow moving them all to a different subdirectory.)
333
334 The Delete buttons lead to a page to confirm the deletion(s).
335
336 The Rename buttons lead to a page with a text edit box for editing the
337 page name. The title of the page is edited, not the actual filename.
338
339 There will also be a optional comment field, so a commit message can be
340 written for the rename/delete.
341
342 Note that there's an edge case concerning pages that have a "/" encoded
343 as part of their title. There's no way for a title edit box to
344 differentiate between that, and a "/" that is instended to refer to a
345 subdirectory to move the page to. Consequence is that "/" will always be
346 treated literally, as a subdir separator; it will not be possible to use
347 this interface to put an encoded "/" in a page's name.
348
349 Once a page is renamed, ikiwiki will return to the page edit interface,
350 now for the renamed page. Any modifications that the user had made to the
351 textarea will be preserved.
352
353 Similarly, when an attachment is renamed, or deleted, return to the page
354 edit interface (with the attachments displayed).
355
356 When a page is deleted, redirect the user to the toplevel index.
357
358 Note that this design, particularly the return to the edit interface after
359 rename, means that the rename button can *only* be put on the page edit ui.
360 It won't be possible to put it on the action bar or somewhere else. (It
361 would be possible to code up a different rename button that doesn't do
362 that, and use it elsewhere.)
363
364 ## SubPages
365
366 When renaming `foo`, it probably makes sense to also rename
367 `foo/Discussion`. Should other SubPages in `foo/` also be renamed? I think
368 it's probably simplest to rename all of its SubPages too.
369
370 When deleting `foo`, I don't think SubPages should be deleted. The
371 potential for mistakes and abuse is too large.
372
373 ## link fixups
374
375 When renaming a page, it's desirable to keep links that point to it
376 working. Rather than use redirection pages, I think that all pages that
377 link to it should be modified to fix their links.
378
379 In some cases, a redirection page will be wanted, to keep long-lived urls
380 working. Since the meta plugin supports creating such pages, and since they
381 won't always be needed, I think it will be simplest to just leave it up to
382 the user to create such a redirection page after renaming a page.
383
384 ## who can delete/rename what?
385
386 The source page must be editable by the user to be deleted/renamed.
387 When renaming, the dest page must not already exist, and must be creatable
388 by the user, too.
389
390 When deleting/renaming attachments, the `allowed_attachments` PageSpec
391 is checked too.
392
393 ## RCS
394
395 Two new optional functions are added to the RCS interface:
396
397 * `rcs_delete(file, message, rcstoken, user, ipaddr)`
398 * `rcs_rename(old, new, message, rcstoken, user, ipaddr)`
399
400 The page move/rename code will check if these are not available, and error
401 out.
402
403 Similar to `rcs_commit` both of these take a rcstoken, which is generated
404 by an earlier `rcs_prepedit`.
405
406 ## conflicts
407
408 Cases that have to be dealt with:
409
410 * Alice clicks "delete" button for a page; Bob makes a modification;
411   Alice confirms deletion. Ideally in this case, Alice should get an error
412   message that there's a conflict.
413 * Alice opens edit UI for a page; Bob makes a modification; Alice
414   clicks delete button and confirms deletion. Again here, Alice should get
415   a conflict error. Note that this means that the rcstoken should be
416   recorded when the edit UI is first opened, not when the delete button is
417   hit.
418 * Alice and Bob both try to delete a page at the same time. It's fine for
419   the second one to get a message that it no longer exists. Or just to
420   silently fail to delete the deleted page..
421 * Alice deletes a page; Bob had edit window open for it, and saves
422   it afterwards. I think that Bob should win in this case; Alice can always
423   notice the page has been added back, and delete it again.
424 * Alice clicks "rename" button for a page; Bob makes a modification;
425   Alice confirms rename. This case seems easy, it should just rename the
426   modified page.
427 * Alice opens edit UI for a page; Bob makes a modification; Alice
428   clicks rename button and confirms rename. Seems same as previous case.
429 * Alice and Bob both try to rename a page at the same time (to probably
430   different names). Or one tries to delete, and the other to rename. 
431   I think it's acceptible for the second one to get an error message that
432   the page no longer exists.
433 * Alice renames a page; Bob had edit window open for it, and saves
434   it afterwards, under old name. I think it's acceptible for Bob to succeed
435   in saving it under the old name in this case, though not ideal.
436 * Alice renames (or deletes) a page. In the meantime, Bob is uploading an
437   attachment to it, and finishes after the rename finishes. Is it
438   acceptible for the attachment to be saved under the old name?
439 * Alice starts creating a new page. In the meantime, Bob renames a
440   different page to that name. Alice should get an error message when
441   committing; and it should have conflict markers. Ie, this should work the
442   same as if Bob had edited the new page at the same time as Alice did.
443 * Bob starts renaming a page. In the meantime, Alice creates a new page
444   with the name he's renaming it to. Here Bob should get a error message
445   that he can't rename the page to an existing name. (A conflict resolution
446   edit would also be ok.)