* Allow editing a page and deleting all content, while still disallowing
[ikiwiki.git] / IkiWiki / CGI.pm
1 #!/usr/bin/perl
2
3 use warnings;
4 use strict;
5 use IkiWiki;
6 use IkiWiki::UserInfo;
7 use open qw{:utf8 :std};
8 use Encode;
9
10 package IkiWiki;
11
12 sub printheader ($) { #{{{
13         my $session=shift;
14         
15         if ($config{sslcookie}) {
16                 print $session->header(-charset => 'utf-8',
17                         -cookie => $session->cookie(-secure => 1));
18         } else {
19                 print $session->header(-charset => 'utf-8');
20         }
21
22 } #}}}
23         
24 sub showform ($$$$) { #{{{
25         my $form=shift;
26         my $buttons=shift;
27         my $session=shift;
28         my $cgi=shift;
29
30         if (exists $hooks{formbuilder}) {
31                 run_hooks(formbuilder => sub {
32                         shift->(form => $form, cgi => $cgi, session => $session,
33                                 buttons => $buttons);
34                 });
35         }
36
37         printheader($session);
38         print misctemplate($form->title, $form->render(submit => $buttons));
39 }
40
41 sub redirect ($$) { #{{{
42         my $q=shift;
43         my $url=shift;
44         if (! $config{w3mmode}) {
45                 print $q->redirect($url);
46         }
47         else {
48                 print "Content-type: text/plain\n";
49                 print "W3m-control: GOTO $url\n\n";
50         }
51 } #}}}
52
53 sub check_canedit ($$$;$) { #{{{
54         my $page=shift;
55         my $q=shift;
56         my $session=shift;
57         my $nonfatal=shift;
58         
59         my $canedit;
60         run_hooks(canedit => sub {
61                 return if defined $canedit;
62                 my $ret=shift->($page, $q, $session);
63                 if (defined $ret && $ret eq "") {
64                         $canedit=1;
65                 }
66                 elsif (defined $ret) {
67                         $canedit=0;
68                         error($ret) unless $nonfatal;
69                 }
70         });
71         return $canedit;
72 } #}}}
73
74 sub decode_form_utf8 ($) { #{{{
75         my $form = shift;
76         foreach my $f ($form->field) {
77                 next if Encode::is_utf8(scalar $form->field($f));
78                 $form->field(name  => $f,
79                              value => decode_utf8($form->field($f)),
80                              force => 1,
81                             );
82         }
83 } #}}}
84
85 sub cgi_recentchanges ($) { #{{{
86         my $q=shift;
87         
88         # Optimisation: building recentchanges means calculating lots of
89         # links. Memoizing htmllink speeds it up a lot (can't be memoized
90         # during page builds as the return values may change, but they
91         # won't here.)
92         eval q{use Memoize};
93         error($@) if $@;
94         memoize("htmllink");
95
96         eval q{use Time::Duration};
97         error($@) if $@;
98
99         my $changelog=[rcs_recentchanges(100)];
100         foreach my $change (@$changelog) {
101                 $change->{when} = concise(ago($change->{when}));
102
103                 $change->{user} = userlink($change->{user});
104
105                 my $is_excess = exists $change->{pages}[10]; # limit pages to first 10
106                 delete @{$change->{pages}}[10 .. @{$change->{pages}}] if $is_excess;
107                 $change->{pages} = [
108                         map {
109                                 $_->{link} = htmllink("", "", $_->{page},
110                                         noimageinline => 1,
111                                         linktext => pagetitle($_->{page}));
112                                 $_;
113                         } @{$change->{pages}}
114                 ];
115                 push @{$change->{pages}}, { link => '...' } if $is_excess;
116         }
117
118         my $template=template("recentchanges.tmpl"); 
119         $template->param(
120                 title => "RecentChanges",
121                 indexlink => indexlink(),
122                 wikiname => $config{wikiname},
123                 changelog => $changelog,
124                 baseurl => baseurl(),
125         );
126         run_hooks(pagetemplate => sub {
127                 shift->(page => "", destpage => "", template => $template);
128         });
129         print $q->header(-charset => 'utf-8'), $template->output;
130 } #}}}
131
132 # Check if the user is signed in. If not, redirect to the signin form and
133 # save their place to return to later.
134 sub needsignin ($$) { #{{{
135         my $q=shift;
136         my $session=shift;
137
138         if (! defined $session->param("name") ||
139             ! userinfo_get($session->param("name"), "regdate")) {
140                 if (! defined $session->param("postsignin")) {
141                         $session->param(postsignin => $ENV{QUERY_STRING});
142                 }
143                 cgi_signin($q, $session);
144                 cgi_savesession($session);
145                 exit;
146         }
147 } #}}}  
148
149 sub cgi_signin ($$) { #{{{
150         my $q=shift;
151         my $session=shift;
152
153         eval q{use CGI::FormBuilder};
154         error($@) if $@;
155         my $form = CGI::FormBuilder->new(
156                 title => "signin",
157                 name => "signin",
158                 charset => "utf-8",
159                 method => 'POST',
160                 required => 'NONE',
161                 javascript => 0,
162                 params => $q,
163                 action => $config{cgiurl},
164                 header => 0,
165                 template => {type => 'div'},
166                 stylesheet => baseurl()."style.css",
167         );
168         my $buttons=["Login"];
169         
170         if ($q->param("do") ne "signin" && !$form->submitted) {
171                 $form->text(gettext("You need to log in first."));
172         }
173         $form->field(name => "do", type => "hidden", value => "signin",
174                 force => 1);
175         
176         decode_form_utf8($form);
177         
178         run_hooks(formbuilder_setup => sub {
179                 shift->(form => $form, cgi => $q, session => $session,
180                         buttons => $buttons);
181         });
182
183         if ($form->submitted) {
184                 $form->validate;
185         }
186
187         showform($form, $buttons, $session, $q);
188 } #}}}
189
190 sub cgi_postsignin ($$) { #{{{
191         my $q=shift;
192         my $session=shift;
193         
194         # Continue with whatever was being done before the signin process.
195         if (defined $session->param("postsignin")) {
196                 my $postsignin=CGI->new($session->param("postsignin"));
197                 $session->clear("postsignin");
198                 cgi($postsignin, $session);
199                 cgi_savesession($session);
200                 exit;
201         }
202         else {
203                 # This can occur, for example, if a user went to the signin
204                 # url via a bookmark.
205                 redirect($q, $config{url});
206         }
207 } #}}}
208
209 sub cgi_prefs ($$) { #{{{
210         my $q=shift;
211         my $session=shift;
212
213         needsignin($q, $session);
214
215         eval q{use CGI::FormBuilder};
216         error($@) if $@;
217         my $form = CGI::FormBuilder->new(
218                 title => "preferences",
219                 name => "preferences",
220                 header => 0,
221                 charset => "utf-8",
222                 method => 'POST',
223                 validate => {
224                         email => 'EMAIL',
225                 },
226                 required => 'NONE',
227                 javascript => 0,
228                 params => $q,
229                 action => $config{cgiurl},
230                 template => {type => 'div'},
231                 stylesheet => baseurl()."style.css",
232                 fieldsets => [
233                         [login => gettext("Login")],
234                         [preferences => gettext("Preferences")],
235                         [admin => gettext("Admin")]
236                 ],
237         );
238         my $buttons=["Save Preferences", "Logout", "Cancel"];
239
240         decode_form_utf8($form);
241
242         run_hooks(formbuilder_setup => sub {
243                 shift->(form => $form, cgi => $q, session => $session,
244                         buttons => $buttons);
245         });
246         
247         $form->field(name => "do", type => "hidden");
248         $form->field(name => "email", size => 50, fieldset => "preferences");
249         $form->field(name => "subscriptions", size => 50,
250                 fieldset => "preferences",
251                 comment => "(".htmllink("", "", "ikiwiki/PageSpec", noimageinline => 1).")");
252         $form->field(name => "banned_users", size => 50,
253                 fieldset => "admin");
254         
255         my $user_name=$session->param("name");
256         if (! is_admin($user_name)) {
257                 $form->field(name => "banned_users", type => "hidden");
258         }
259
260         if (! $form->submitted) {
261                 $form->field(name => "email", force => 1,
262                         value => userinfo_get($user_name, "email"));
263                 $form->field(name => "subscriptions", force => 1,
264                         value => userinfo_get($user_name, "subscriptions"));
265                 if (is_admin($user_name)) {
266                         $form->field(name => "banned_users", force => 1,
267                                 value => join(" ", get_banned_users()));
268                 }
269         }
270         
271         if ($form->submitted eq 'Logout') {
272                 $session->delete();
273                 redirect($q, $config{url});
274                 return;
275         }
276         elsif ($form->submitted eq 'Cancel') {
277                 redirect($q, $config{url});
278                 return;
279         }
280         elsif ($form->submitted eq 'Save Preferences' && $form->validate) {
281                 foreach my $field (qw(email subscriptions)) {
282                         if (defined $form->field($field) && length $form->field($field)) {
283                                 userinfo_set($user_name, $field, $form->field($field)) ||
284                                         error("failed to set $field");
285                         }
286                 }
287                 if (is_admin($user_name)) {
288                         set_banned_users(grep { ! is_admin($_) }
289                                         split(' ',
290                                                 $form->field("banned_users"))) ||
291                                 error("failed saving changes");
292                 }
293                 $form->text(gettext("Preferences saved."));
294         }
295         
296         showform($form, $buttons, $session, $q);
297 } #}}}
298
299 sub cgi_editpage ($$) { #{{{
300         my $q=shift;
301         my $session=shift;
302
303         my @fields=qw(do rcsinfo subpage from page type editcontent comments);
304         my @buttons=("Save Page", "Preview", "Cancel");
305         
306         eval q{use CGI::FormBuilder};
307         error($@) if $@;
308         my $form = CGI::FormBuilder->new(
309                 title => "editpage",
310                 fields => \@fields,
311                 charset => "utf-8",
312                 method => 'POST',
313                 required => [qw{editcontent}],
314                 javascript => 0,
315                 params => $q,
316                 action => $config{cgiurl},
317                 header => 0,
318                 table => 0,
319                 template => scalar template_params("editpage.tmpl"),
320                 wikiname => $config{wikiname},
321         );
322         
323         decode_form_utf8($form);
324         
325         run_hooks(formbuilder_setup => sub {
326                 shift->(form => $form, cgi => $q, session => $session,
327                         buttons => \@buttons);
328         });
329         
330         # This untaint is safe because titlepage removes any problematic
331         # characters.
332         my ($page)=$form->field('page');
333         $page=titlepage(possibly_foolish_untaint($page));
334         if (! defined $page || ! length $page ||
335             file_pruned($page, $config{srcdir}) || $page=~/^\//) {
336                 error("bad page name");
337         }
338         
339         my $from;
340         if (defined $form->field('from')) {
341                 ($from)=$form->field('from')=~/$config{wiki_file_regexp}/;
342         }
343         
344         my $file;
345         my $type;
346         if (exists $pagesources{$page} && $form->field("do") ne "create") {
347                 $file=$pagesources{$page};
348                 $type=pagetype($file);
349                 if (! defined $type) {
350                         error(sprintf(gettext("%s is not an editable page"), $page));
351                 }
352                 if (! $form->submitted) {
353                         $form->field(name => "rcsinfo",
354                                 value => rcs_prepedit($file), force => 1);
355                 }
356                 $form->field(name => "editcontent", validate => '/.*/');
357         }
358         else {
359                 $type=$form->param('type');
360                 if (defined $type && length $type && $hooks{htmlize}{$type}) {
361                         $type=possibly_foolish_untaint($type);
362                 }
363                 elsif (defined $from) {
364                         # favor the type of linking page
365                         $type=pagetype($pagesources{$from});
366                 }
367                 $type=$config{default_pageext} unless defined $type;
368                 $file=$page.".".$type;
369                 if (! $form->submitted) {
370                         $form->field(name => "rcsinfo", value => "", force => 1);
371                 }
372                 $form->field(name => "editcontent", validate => '/.+/');
373         }
374
375         $form->field(name => "do", type => 'hidden');
376         $form->field(name => "from", type => 'hidden');
377         $form->field(name => "rcsinfo", type => 'hidden');
378         $form->field(name => "subpage", type => 'hidden');
379         $form->field(name => "page", value => pagetitle($page, 1), force => 1);
380         $form->field(name => "type", value => $type, force => 1);
381         $form->field(name => "comments", type => "text", size => 80);
382         $form->field(name => "editcontent", type => "textarea", rows => 20,
383                 cols => 80);
384         $form->tmpl_param("can_commit", $config{rcs});
385         $form->tmpl_param("indexlink", indexlink());
386         $form->tmpl_param("helponformattinglink",
387                 htmllink("", "", "ikiwiki/formatting",
388                         noimageinline => 1,
389                         linktext => "FormattingHelp"));
390         $form->tmpl_param("baseurl", baseurl());
391         
392         if ($form->submitted eq "Cancel") {
393                 if ($form->field("do") eq "create" && defined $from) {
394                         redirect($q, "$config{url}/".htmlpage($from));
395                 }
396                 elsif ($form->field("do") eq "create") {
397                         redirect($q, $config{url});
398                 }
399                 else {
400                         redirect($q, "$config{url}/".htmlpage($page));
401                 }
402                 return;
403         }
404         elsif ($form->submitted eq "Preview") {
405                 my $content=$form->field('editcontent');
406                 run_hooks(editcontent => sub {
407                         $content=shift->(
408                                 content => $content,
409                                 page => $page,
410                                 cgi => $q,
411                                 session => $session,
412                         );
413                 });
414                 $form->tmpl_param("page_preview",
415                         htmlize($page, $type,
416                         linkify($page, "",
417                         preprocess($page, $page,
418                         filter($page, $page, $content), 0, 1))));
419         }
420         elsif ($form->submitted eq "Save Page") {
421                 $form->tmpl_param("page_preview", "");
422         }
423         $form->tmpl_param("page_conflict", "");
424         
425         if ($form->submitted ne "Save Page" || ! $form->validate) {
426                 if ($form->field("do") eq "create") {
427                         my @page_locs;
428                         my $best_loc;
429                         if (! defined $from || ! length $from ||
430                             $from ne $form->field('from') ||
431                             file_pruned($from, $config{srcdir}) ||
432                             $from=~/^\// ||
433                             $form->submitted eq "Preview") {
434                                 @page_locs=$best_loc=$page;
435                         }
436                         else {
437                                 my $dir=$from."/";
438                                 $dir=~s![^/]+/+$!!;
439                                 
440                                 if ((defined $form->field('subpage') && length $form->field('subpage')) ||
441                                     $page eq gettext('discussion')) {
442                                         $best_loc="$from/$page";
443                                 }
444                                 else {
445                                         $best_loc=$dir.$page;
446                                 }
447                                 
448                                 push @page_locs, $dir.$page;
449                                 push @page_locs, "$from/$page";
450                                 while (length $dir) {
451                                         $dir=~s![^/]+/+$!!;
452                                         push @page_locs, $dir.$page;
453                                 }
454                         }
455                         push @page_locs, "$config{userdir}/$page"
456                                 if length $config{userdir};
457
458                         @page_locs = grep {
459                                 ! exists $pagecase{lc $_}
460                         } @page_locs;
461                         if (! @page_locs) {
462                                 # hmm, someone else made the page in the
463                                 # meantime?
464                                 redirect($q, "$config{url}/".htmlpage($page));
465                                 return;
466                         }
467
468                         my @editable_locs = grep {
469                                 check_canedit($_, $q, $session, 1)
470                         } @page_locs;
471                         if (! @editable_locs) {
472                                 # let it throw an error this time
473                                 map { check_canedit($_, $q, $session) } @page_locs;
474                         }
475                         
476                         my @page_types;
477                         if (exists $hooks{htmlize}) {
478                                 @page_types=keys %{$hooks{htmlize}};
479                         }
480                         
481                         $form->tmpl_param("page_select", 1);
482                         $form->field(name => "page", type => 'select',
483                                 options => [ map { pagetitle($_, 1) } @editable_locs ],
484                                 value => pagetitle($best_loc, 1));
485                         $form->field(name => "type", type => 'select',
486                                 options => \@page_types);
487                         $form->title(sprintf(gettext("creating %s"), pagetitle($page)));
488                         
489                 }
490                 elsif ($form->field("do") eq "edit") {
491                         check_canedit($page, $q, $session);
492                         if (! defined $form->field('editcontent') || 
493                             ! length $form->field('editcontent')) {
494                                 my $content="";
495                                 if (exists $pagesources{$page}) {
496                                         $content=readfile(srcfile($pagesources{$page}));
497                                         $content=~s/\n/\r\n/g;
498                                 }
499                                 $form->field(name => "editcontent", value => $content,
500                                         force => 1);
501                         }
502                         $form->tmpl_param("page_select", 0);
503                         $form->field(name => "page", type => 'hidden');
504                         $form->field(name => "type", type => 'hidden');
505                         $form->title(sprintf(gettext("editing %s"), pagetitle($page)));
506                 }
507                 
508                 showform($form, \@buttons, $session, $q);
509                 saveindex();
510         }
511         else {
512                 # save page
513                 check_canedit($page, $q, $session);
514
515                 my $exists=-e "$config{srcdir}/$file";
516
517                 if ($form->field("do") ne "create" && ! $exists &&
518                     ! eval { srcfile($file) }) {
519                         $form->tmpl_param("page_gone", 1);
520                         $form->field(name => "do", value => "create", force => 1);
521                         $form->tmpl_param("page_select", 0);
522                         $form->field(name => "page", type => 'hidden');
523                         $form->field(name => "type", type => 'hidden');
524                         $form->title(sprintf(gettext("editing %s"), $page));
525                         showform($form, \@buttons, $session, $q);
526                         return;
527                 }
528                 elsif ($form->field("do") eq "create" && $exists) {
529                         $form->tmpl_param("creation_conflict", 1);
530                         $form->field(name => "do", value => "edit", force => 1);
531                         $form->tmpl_param("page_select", 0);
532                         $form->field(name => "page", type => 'hidden');
533                         $form->field(name => "type", type => 'hidden');
534                         $form->title(sprintf(gettext("editing %s"), $page));
535                         $form->field("editcontent", 
536                                 value => readfile("$config{srcdir}/$file").
537                                          "\n\n\n".$form->field("editcontent"),
538                                 force => 1);
539                         showform($form, \@buttons, $session, $q);
540                         return;
541                 }
542                 
543                 my $content=$form->field('editcontent');
544                 run_hooks(editcontent => sub {
545                         $content=shift->(
546                                 content => $content,
547                                 page => $page,
548                                 cgi => $q,
549                                 session => $session,
550                         );
551                 });
552                 $content=~s/\r\n/\n/g;
553                 $content=~s/\r/\n/g;
554                 $content.="\n" if $content !~ /\n$/;
555
556                 $config{cgi}=0; # avoid cgi error message
557                 eval { writefile($file, $config{srcdir}, $content) };
558                 $config{cgi}=1;
559                 if ($@) {
560                         $form->field(name => "rcsinfo", value => rcs_prepedit($file),
561                                 force => 1);
562                         $form->tmpl_param("failed_save", 1);
563                         $form->tmpl_param("error_message", $@);
564                         $form->field("editcontent", value => $content, force => 1);
565                         $form->tmpl_param("page_select", 0);
566                         $form->field(name => "page", type => 'hidden');
567                         $form->field(name => "type", type => 'hidden');
568                         $form->title(sprintf(gettext("editing %s"), $page));
569                         showform($form, \@buttons, $session, $q);
570                         return;
571                 }
572                 
573                 my $conflict;
574                 if ($config{rcs}) {
575                         my $message="";
576                         if (defined $form->field('comments') &&
577                             length $form->field('comments')) {
578                                 $message=$form->field('comments');
579                         }
580                         
581                         if (! $exists) {
582                                 rcs_add($file);
583                         }
584
585                         # Prevent deadlock with post-commit hook by
586                         # signaling to it that it should not try to
587                         # do anything (except send commit mails).
588                         disable_commit_hook();
589                         $conflict=rcs_commit($file, $message,
590                                 $form->field("rcsinfo"),
591                                 $session->param("name"), $ENV{REMOTE_ADDR});
592                         enable_commit_hook();
593                         rcs_update();
594                 }
595                 
596                 # Refresh even if there was a conflict, since other changes
597                 # may have been committed while the post-commit hook was
598                 # disabled.
599                 require IkiWiki::Render;
600                 # Reload index, since the first time it's loaded is before
601                 # the wiki is locked, and things may have changed in the
602                 # meantime.
603                 loadindex();
604                 refresh();
605                 saveindex();
606
607                 if (defined $conflict) {
608                         $form->field(name => "rcsinfo", value => rcs_prepedit($file),
609                                 force => 1);
610                         $form->tmpl_param("page_conflict", 1);
611                         $form->field("editcontent", value => $conflict, force => 1);
612                         $form->field("do", "edit", force => 1);
613                         $form->tmpl_param("page_select", 0);
614                         $form->field(name => "page", type => 'hidden');
615                         $form->field(name => "type", type => 'hidden');
616                         $form->title(sprintf(gettext("editing %s"), $page));
617                         showform($form, \@buttons, $session, $q);
618                         return;
619                 }
620                 else {
621                         # The trailing question mark tries to avoid broken
622                         # caches and get the most recent version of the page.
623                         redirect($q, "$config{url}/".htmlpage($page)."?updated");
624                 }
625         }
626 } #}}}
627
628 sub cgi_getsession ($) { #{{{
629         my $q=shift;
630
631         eval q{use CGI::Session};
632         CGI::Session->name("ikiwiki_session_".encode_utf8($config{wikiname}));
633         
634         my $oldmask=umask(077);
635         my $session = CGI::Session->new("driver:DB_File", $q,
636                 { FileName => "$config{wikistatedir}/sessions.db" });
637         umask($oldmask);
638
639         return $session;
640 } #}}}
641
642 sub cgi_savesession ($) { #{{{
643         my $session=shift;
644
645         # Force session flush with safe umask.
646         my $oldmask=umask(077);
647         $session->flush;
648         umask($oldmask);
649 } #}}}
650
651 sub cgi (;$$) { #{{{
652         my $q=shift;
653         my $session=shift;
654
655         if (! $q) {
656                 eval q{use CGI};
657                 error($@) if $@;
658         
659                 $q=CGI->new;
660         
661                 run_hooks(cgi => sub { shift->($q) });
662         }
663
664         my $do=$q->param('do');
665         if (! defined $do || ! length $do) {
666                 my $error = $q->cgi_error;
667                 if ($error) {
668                         error("Request not processed: $error");
669                 }
670                 else {
671                         error("\"do\" parameter missing");
672                 }
673         }
674         
675         # Things that do not need a session.
676         if ($do eq 'recentchanges') {
677                 cgi_recentchanges($q);
678                 return;
679         }
680
681         # Need to lock the wiki before getting a session.
682         lockwiki();
683         
684         if (! $session) {
685                 $session=cgi_getsession($q);
686         }
687         
688         # Auth hooks can sign a user in.
689         if ($do ne 'signin' && ! defined $session->param("name")) {
690                 run_hooks(auth => sub {
691                         shift->($q, $session)
692                 });
693                 if (defined $session->param("name")) {
694                         # Make sure whatever user was authed is in the
695                         # userinfo db.
696                         if (! userinfo_get($session->param("name"), "regdate")) {
697                                 userinfo_setall($session->param("name"), {
698                                         email => "",
699                                         password => "",
700                                         regdate => time,
701                                 }) || error("failed adding user");
702                         }
703                 }
704         }
705         
706         if (defined $session->param("name") &&
707             userinfo_get($session->param("name"), "banned")) {
708                 print $q->header(-status => "403 Forbidden");
709                 $session->delete();
710                 print gettext("You are banned.");
711                 cgi_savesession($session);
712         }
713
714         run_hooks(sessioncgi => sub { shift->($q, $session) });
715
716         if ($do eq 'signin') {
717                 cgi_signin($q, $session);
718                 cgi_savesession($session);
719         }
720         elsif (defined $session->param("postsignin")) {
721                 cgi_postsignin($q, $session);
722         }
723         elsif ($do eq 'prefs') {
724                 cgi_prefs($q, $session);
725         }
726         elsif ($do eq 'create' || $do eq 'edit') {
727                 cgi_editpage($q, $session);
728         }
729         elsif ($do eq 'postsignin') {
730                 error(gettext("login failed, perhaps you need to turn on cookies?"));
731         }
732         else {
733                 error("unknown do parameter");
734         }
735 } #}}}
736
737 sub userlink ($) { #{{{
738         my $user=shift;
739
740         eval q{use CGI 'escapeHTML'};
741         error($@) if $@;
742         if ($user =~ m!^https?://! &&
743             eval q{use Net::OpenID::VerifiedIdentity; 1} && !$@) {
744                 # Munge user-urls, as used by eg, OpenID.
745                 my $oid=Net::OpenID::VerifiedIdentity->new(identity => $user);
746                 my $display=$oid->display;
747                 # Convert "user.somehost.com" to "user [somehost.com]".
748                 if ($display !~ /\[/) {
749                         $display=~s/^(.*?)\.([^.]+\.[a-z]+)$/$1 [$2]/;
750                 }
751                 # Convert "http://somehost.com/user" to "user [somehost.com]".
752                 if ($display !~ /\[/) {
753                         $display=~s/^https?:\/\/(.+)\/([^\/]+)$/$2 [$1]/;
754                 }
755                 $display=~s!^https?://!!; # make sure this is removed
756                 return "<a href=\"$user\">".escapeHTML($display)."</a>";
757         }
758         else {
759                 return htmllink("", "", escapeHTML(
760                         length $config{userdir} ? $config{userdir}."/".$user : $user
761                 ), noimageinline => 1);
762         }
763 } #}}}
764
765 1