Handle hostnames with upper-case letters
[webmin.git] / Webmin / Table.pm
1 package Webmin::Table;
2 use Webmin::JavascriptButton;
3 use WebminCore;
4
5 =head2 new Webmin::Table(&headings, [width], [name], [heading])
6 Create a multi-column table, with support for sorting, paging and so on
7 =cut
8 sub new
9 {
10 if (defined(&Webmin::Theme::Table::new) &&
11     caller() !~ /Webmin::Theme::Table/) {
12         return new Webmin::Theme::Table(@_[1..$#_]);
13         }
14 my ($self, $headings, $width, $name, $heading) = @_;
15 $self = { 'sorter' => [ map { \&default_sorter } @$headings ] };
16 bless($self);
17 $self->set_headings($headings);
18 $self->set_name($name) if (defined($name));
19 $self->set_width($width) if (defined($width));
20 $self->set_heading($heading) if (defined($heading));
21 $self->set_all_sortable(1);
22 $self->set_paging(1);
23 return $self;
24 }
25
26 =head2 add_row(&fields)
27 Adds a row to the table. Each element in the row can be either an input of some
28 kind, or a piece of text.
29 =cut
30 sub add_row
31 {
32 my ($self, $fields) = @_;
33 push(@{$self->{'rows'}}, $fields);
34 }
35
36 =head2 html()
37 Returns the HTML for this table. The actual ordering may depend upon sort headers
38 clicked by the user. The rows to display may be limited by the page size.
39 =cut
40 sub html
41 {
42 my ($self) = @_;
43 my @srows = @{$self->{'rows'}};
44 my $thisurl = $self->{'form'}->{'page'}->get_myurl();
45 my $name = $self->get_name();
46 my $rv;
47
48 # Add the heading
49 if ($self->get_heading()) {
50         $rv .= &ui_subheading($self->get_heading())."\n";
51         }
52
53 my $sm = $self->get_searchmax();
54 if (defined($sm) && @srows > $sm) {
55         # Too many rows to show .. add a search form. This will need to close
56         # the parent form, and then re-open it after the search form, as nested
57         # forms aren't allowed!
58         if ($self->get_searchmsg()) {
59                 $rv .= $self->get_searchmsg()."<br>\n";
60                 }
61
62         my $form = new Webmin::Form($thisurl, "get");
63         $form->set_input($self->{'form'}->{'in'});
64         my $section = new Webmin::Section(undef, 2);
65         $form->add_section($section);
66
67         my $col = new Webmin::Select("ui_searchcol_".$name, undef);
68         my $i = 0;
69         foreach my $h (@{$self->get_headings()}) {
70                 if ($self->{'sortable'}->[$i]) {
71                         $col->add_option($i, $h);
72                         }
73                 $i++;
74                 }
75         $section->add_input($text{'ui_searchcol'}, $col);
76
77         my $for = new Webmin::Textbox("ui_searchfor_".$name, undef, 30);
78         $section->add_input($text{'ui_searchfor'}, $for);
79
80         $rv .= $section->html();
81         my $url = $self->make_url(undef, undef, undef, undef, 1);
82         my $jsb = new Webmin::JavascriptButton($text{'ui_searchok'},
83                         "window.location = '$url'+'&'+'ui_searchfor_${name}'+'='+escape(form.ui_searchfor_${name}.value)+'&'+'ui_searchcol_${name}'+'='+escape(form.ui_searchcol_${name}.selectedIndex)");
84         $rv .= $jsb->html();
85         $rv .= "<br>\n";
86
87         # Limit records to current search
88         if (defined($col->get_value())) {
89                 my $sf = $for->get_value();
90                 @srows = grep { $_->[$col->get_value()] =~ /\Q$sf\E/i } @srows;
91                 }
92         else {
93                 @srows = ( );
94                 }
95         }
96
97 # Prepare the selector
98 my $selc = $self->{'selectcolumn'};
99 my $seli = $self->{'selectinput'};
100 my %selmap;
101 if (defined($selc)) {
102         my $i = 0;
103         foreach my $r (@srows) {
104                 $selmap{$r,$selc} = $seli->one_html($i);
105                 $i++;
106                 }
107         }
108
109 # Sort the rows
110 my ($sortcol, $sortdir) = $self->get_sortcolumn();
111 if (defined($sortcol)) {
112         my $func = $self->{'sorter'}->[$sortcol];
113         @srows = sort { my $so = &$func($a->[$sortcol], $b->[$sortcol], $sortcol);
114                         $sortdir ? -$so : $so } @srows;
115         }
116
117 # Build the td attributes
118 my @tds = map { "valign=top" } @{$self->{'headings'}};
119 if ($self->{'widths'}) {
120         my $i = 0;
121         foreach my $w (@{$self->{'widths'}}) {
122                 $tds[$i++] .= " width=$w";
123                 }
124         }
125 if ($self->{'aligns'}) {
126         my $i = 0;
127         foreach my $a (@{$self->{'aligns'}}) {
128                 $tds[$i++] .= " align=$a";
129                 }
130         }
131
132 # Find the page we want
133 my $page = $self->get_pagepos();
134 my ($start, $end, $origsize);
135 if ($self->get_paging() && $self->get_pagesize()) {
136         # Restrict view to rows within some page
137         $start = $self->get_pagesize()*$page;
138         $end = $self->get_pagesize()*($page+1) - 1;
139         if ($start >= @srows) {
140                 # Gone off end!
141                 $start = 0;
142                 $end = $self->get_pagesize()-1;
143                 }
144         if ($end >= @srows) {
145                 # End is too far
146                 $end = @srows-1;
147                 }
148         $origsize = scalar(@srows);
149         @srows = @srows[$start..$end];
150         }
151
152 # Generate the headings, with sorters
153 $thisurl .= $thisurl =~ /\?/ ? "&" : "?";
154 my @sheadings;
155 my $i = 0;
156 foreach my $h (@{$self->get_headings()}) {
157         if ($self->{'sortable'}->[$i]) {
158                 # Column can be sorted!
159                 my $hh = "<table cellpadding=0 cellspacing=0 width=100%><tr>";
160                 $hh .= "<td><b>$h</b></td> <td align=right>";
161                 if (!defined($sortcol) || $sortcol != $i) {
162                         # Not sorting on this column .. show grey button
163                         my $url = $self->make_url($i, 0, undef, undef);
164                         $hh .= "<a href='$url'>".
165                                "<img src=$gconfig{'webprefix'}/images/nosort.gif border=0></a>";
166                         }
167                 else {
168                         # Sorting .. show button to switch mode
169                         my $notsort = !$sortdir;
170                         my $url = $self->make_url($i, $sortdir ? 0 : 1, undef, undef);
171                         $hh .= "<a href='$url'>".
172                                "<img src=$gconfig{'webprefix'}/images/sort.gif border=0></a>";
173                         }
174                 $hh .= "</td></tr></table>";
175                 push(@sheadings, $hh);
176                 }
177         else {
178                 push(@sheadings, $h);
179                 }
180         $i++;
181         }
182
183 # Get any errors for inputs
184 my @errs = map { $_->get_errors() } $self->list_inputs();
185 if (@errs) {
186         foreach my $e (@errs) {
187                 $rv .= "<font color=#ff0000>$e</font><br>\n";
188                 }
189         }
190
191 # Build links for top and bottom
192 my $links;
193 if (ref($seli) =~ /Checkboxes/) {
194         # Add select all/none links
195         my $formno = $self->{'form'}->get_formno();
196         $links .= &select_all_link($seli->get_name(), $formno,
197                                          $text{'ui_selall'})."\n";
198         $links .= &select_invert_link($seli->get_name(), $formno,
199                                             $text{'ui_selinv'})."\n";
200         $links .= "&nbsp;\n";
201         }
202 foreach my $l (@{$self->{'links'}}) {
203         $links .= "<a href='$l->[0]'>$l->[1]</a>\n";
204         }
205 $links .= "<br>" if ($links);
206
207 # Build list of inputs for bottom
208 my $inputs;
209 foreach my $i (@{$self->{'inputs'}}) {
210         $inputs .= $i->html()."\n";
211         }
212 $inputs .= "<br>" if ($inputs);
213
214 # Create the pager
215 if ($self->get_paging() && $origsize) {
216         my $lastpage = int(($origsize-1)/$self->get_pagesize());
217         $rv .= "<center>";
218         if ($page != 0) {
219                 # Add start and left arrows
220                 my $surl = $self->make_url(undef, undef, undef, 0);
221                 $rv .= "<a href='$surl'><img src=$gconfig{'webprefix'}/images/first.gif border=0 align=middle></a>\n";
222                 my $lurl = $self->make_url(undef, undef, undef, $page-1);
223                 $rv .= "<a href='$lurl'><img src=$gconfig{'webprefix'}/images/left.gif border=0 align=middle></a>\n";
224                 }
225         else {
226                 # Start and left are disabled
227                 $rv .= "<img src=$gconfig{'webprefix'}/images/first-grey.gif border=0 align=middle>\n";
228                 $rv .= "<img src=$gconfig{'webprefix'}/images/left-grey.gif border=0 align=middle>\n";
229                 }
230         $rv .= &text('ui_paging', $start+1, $end+1, $origsize);
231         if ($end < $origsize-1) {
232                 # Add right and end arrows
233                 my $rurl = $self->make_url(undef, undef, undef, $page+1);
234                 $rv .= "<a href='$rurl'><img src=$gconfig{'webprefix'}/images/right.gif border=0 align=middle></a>\n";
235                 my $eurl = $self->make_url(undef, undef, undef, $lastpage);
236                 $rv .= "<a href='$eurl'><img src=$gconfig{'webprefix'}/images/last.gif border=0 align=middle></a>\n";
237                 }
238         else {
239                 # Right and end are disabled
240                 $rv .= "<img src=$gconfig{'webprefix'}/images/right-grey.gif border=0 align=middle>\n";
241                 $rv .= "<img src=$gconfig{'webprefix'}/images/last-grey.gif border=0 align=middle>\n";
242                 }
243         $rv .= "</center>\n";
244         }
245
246 # Create actual table
247 if (@srows) {
248         $rv .= $links;
249         $rv .= &ui_columns_start(\@sheadings, $self->{'width'}, 0, \@tds);
250         foreach my $r (@srows) {
251                 my @row;
252                 for(my $i=0; $i<@$r || $i<@sheadings; $i++) {
253                         if (ref($r->[$i]) eq "ARRAY") {
254                                 my $j = $r->[$i]->[0] &&
255                                         $r->[$i]->[0]->isa("Webmin::TableAction")
256                                         ? "&nbsp;|&nbsp;" : "&nbsp;";
257                                 $row[$i] = $selmap{$r,$i}.
258                                   join($j, map { ref($_) ? $_->html() : $_ }
259                                                      @{$r->[$i]});
260                                 }
261                         elsif (ref($r->[$i])) {
262                                 $row[$i] = $selmap{$r,$i}.$r->[$i]->html();
263                                 }
264                         else {
265                                 $row[$i] = $selmap{$r,$i}.$r->[$i];
266                                 }
267                         }
268                 $rv .= &ui_columns_row(\@row, \@tds);
269                 }
270         $rv .= &ui_columns_end();
271         }
272 elsif ($self->{'emptymsg'}) {
273         $rv .= $self->{'emptymsg'}."<p>\n";
274         }
275 $rv .= $links;
276 $rv .= $inputs;
277 return $rv;
278 }
279
280 =head2 set_form(form)
281 Called by the Webmin::Form object when this table is added to it
282 =cut
283 sub set_form
284 {
285 my ($self, $form) = @_;
286 $self->{'form'} = $form;
287 foreach my $i ($self->list_inputs()) {
288         $i->set_form($form);
289         }
290 }
291
292 =head2 set_sorter(function, [column])
293 Sets a function used for sorting fields. Will be called with two field values to
294 compare, and a column number.
295 =cut
296 sub set_sorter
297 {
298 my ($self, $func, $col) = @_;
299 if (defined($col)) {
300         $self->{'sorter'}->[$col] = $func;
301         }
302 else {
303         $self->{'sorter'} = [ map { $func } @{$self->{'headings'}} ];
304         }
305 }
306
307 =head2 default_sorter(value1, value2, col)
308 =cut
309 sub default_sorter
310 {
311 my ($value1, $value2, $col) = @_;
312 if (ref($value1) && $value1->isa("Webmin::TableAction")) {
313         $value1 = $value1->get_value();
314         $value2 = $value2->get_value();
315         }
316 return lc($value1) cmp lc($value2);
317 }
318
319 =head2 numeric_sorter(value1, value2, col)
320 =cut
321 sub numeric_sorter
322 {
323 my ($value1, $value2, $col) = @_;
324 return $value1 <=> $value2;
325 }
326
327 =head2 set_sortable(column, sortable?)
328 Tells the table if some column should allow sorting or not. By default, all are.
329 =cut
330 sub set_sortable
331 {
332 my ($self, $col, $sortable) = @_;
333 $self->{'sortable'}->[$col] = $sortable;
334 }
335
336 =head2 set_all_sortable(sortable?)
337 Enabled or disables sorting for all columns
338 =cut
339 sub set_all_sortable
340 {
341 my ($self, $sortable) = @_;
342 $self->{'sortable'} = [ map { $sortable } @{$self->{'headings'}} ];
343 }
344
345 =head2 get_sortcolumn()
346 Returns the column to sort on and the order (1 for descending), or undef for none
347 =cut
348 sub get_sortcolumn
349 {
350 my ($self) = @_;
351 my $in = $self->{'form'} ? $self->{'form'}->{'in'} : undef;
352 my $name = $self->get_name();
353 if ($in && defined($in->{"ui_sortcolumn_".$name})) {
354         return ( $in->{"ui_sortcolumn_".$name},
355                  $in->{"ui_sortdir_".$name} );
356         }
357 else {
358         return ( $self->{'sortcolumn'}, $self->{'sortdir'} );
359         }
360 }
361
362 =head2 set_sortcolumn(num, descending?)
363 Sets the default column on which sorting will be done, unless overridden by
364 the user.
365 =cut
366 sub set_sortcolumn
367 {
368 my ($self, $col, $desc) = @_;
369 $self->{'sortcolumn'} = $col;
370 $self->{'sortdir'} = $desc;
371 }
372
373 =head2 get_paging()
374 Returns 1 if page-by-page display should be used
375 =cut
376 sub get_paging
377 {
378 my ($self) = @_;
379 my $in = $self->{'form'} ? $self->{'form'}->{'in'} : undef;
380 my $name = $self->get_name();
381 if ($in && defined($in->{"ui_paging_".$name})) {
382         return ( $in->{"ui_paging_".$name} );
383         }
384 else {
385         return ( $self->{'paging'} );
386         }
387 }
388
389 =head2 set_paging(paging?)
390 Turns page-by-page display of the table on or off
391 =cut
392 sub set_paging
393 {
394 my ($self, $paging) = @_;
395 $self->{'paging'} = $paging;
396 }
397
398 sub set_name
399 {
400 my ($self, $name) = @_;
401 $self->{'name'} = $name;
402 }
403
404 =head2 get_name()
405 Returns the name for indentifying this table in HTML
406 =cut
407 sub get_name
408 {
409 my ($self) = @_;
410 if (defined($self->{'name'})) {
411         return $self->{'name'};
412         }
413 elsif ($self->{'form'}) {
414         my $secs = $self->{'form'}->{'sections'};
415         for(my $i=0; $i<@$secs; $i++) {
416                 return "table".$i if ($secs->[$i] eq $self);
417                 }
418         }
419 return "table";
420 }
421
422 sub set_headings
423 {
424 my ($self, $headings) = @_;
425 $self->{'headings'} = $headings;
426 }
427
428 sub get_headings
429 {
430 my ($self) = @_;
431 return $self->{'headings'};
432 }
433
434 =head2 set_selector(column, input)
435 Takes a Webmin::Checkboxes or Webmin::Radios object, and uses it to add checkboxes
436 in the specified column.
437 =cut
438 sub set_selector
439 {
440 my ($self, $col, $input) = @_;
441 $self->{'selectcolumn'} = $col;
442 $self->{'selectinput'} = $input;
443 $input->set_form($form);
444 }
445
446 =head2 get_selector()
447 Returns the UI element used for selecting rows
448 =cut
449 sub get_selector
450 {
451 my ($self) = @_;
452 return wantarray ? ( $self->{'selectinput'}, $self->{'selectcolumn'} )
453                  : $self->{'selectinput'};
454 }
455
456 =head2 set_widths(&widths)
457 Given an array reference of widths (like 50 or 20%), uses them for the columns
458 in the table.
459 =cut
460 sub set_widths
461 {
462 my ($self, $widths) = @_;
463 $self->{'widths'} = $widths;
464 }
465
466 =head2 set_width([number|number%])
467 Sets the width of this entire table. Can be called with 100%, 500 or undef to use
468 the minimum possible width.
469 =cut
470 sub set_width
471 {
472 my ($self, $width) = @_;
473 $self->{'width'} = $width;
474 }
475
476 =head2 set_aligns(&aligns)
477 Given an array reference of horizontal alignments (like left or right), uses them
478 for the columns in the table.
479 =cut
480 sub set_aligns
481 {
482 my ($self, $aligns) = @_;
483 $self->{'aligns'} = $aligns;
484 }
485
486 =head2 validate()
487 Validates all inputs, and returns a list of error messages
488 =cut
489 sub validate
490 {
491 my ($self) = @_;
492 my $seli = $self->{'selectinput'};
493 my @errs;
494 if ($seli) {
495         push(@errs, map { [ $seli->get_name(), $_ ] } $seli->validate());
496         }
497 foreach my $i ($self->list_inputs()) {
498         foreach my $e ($i->validate()) {
499                 push(@errs, [ $i->get_name(), $e ]);
500                 }
501         }
502 return @errs;
503 }
504
505 =head2 get_value(input-name)
506 Returns the value of the input with the given name.
507 =cut
508 sub get_value
509 {
510 my ($self, $name) = @_;
511 if ($self->{'selectinput'} && $self->{'selectinput'}->get_name() eq $name) {
512         return $self->{'selectinput'}->get_value();
513         }
514 foreach my $i ($self->list_inputs()) {
515         if ($i->get_name() eq $name) {
516                 return $i->get_value();
517                 }
518         }
519 return undef;
520 }
521
522 =head2 list_inputs()
523 Returns all inputs in all form sections
524 =cut
525 sub list_inputs
526 {
527 my ($self) = @_;
528 my @rv = @{$self->{'inputs'}};
529 push(@rv, $self->{'selectinput'}) if ($self->{'selectinput'});
530 return @rv;
531 }
532
533 =head2 set_searchmax(num, [message])
534 Sets the maximum number of table rows to display before showing a search form
535 =cut
536 sub set_searchmax
537 {
538 my ($self, $searchmax, $searchmsg) = @_;
539 $self->{'searchmax'} = $searchmax;
540 $self->{'searchmsg'} = $searchmsg;
541 }
542
543 sub get_searchmax
544 {
545 my ($self) = @_;
546 return $self->{'searchmax'};
547 }
548
549 sub get_searchmsg
550 {
551 my ($self) = @_;
552 return $self->{'searchmsg'};
553 }
554
555 =head2 add_link(link, message)
556 Adds a link to the table, for example for adding a new entry
557 =cut
558 sub add_link
559 {
560 my ($self, $link, $msg) = @_;
561 push(@{$self->{'links'}}, [ $link, $msg ]);
562 }
563
564 =head2 add_input(input)
565 Adds some input to be displayed at the bottom of the table
566 =cut
567 sub add_input
568 {
569 my ($self, $input) = @_;
570 push(@{$self->{'inputs'}}, $input);
571 $input->set_form($self->{'form'});
572 }
573
574 =head2 set_emptymsg(message)
575 Sets the message to display when the table is empty
576 =cut
577 sub set_emptymsg
578 {
579 my ($self, $emptymsg) = @_;
580 $self->{'emptymsg'} = $emptymsg;
581 }
582
583 =head2 set_heading(text)
584 Sets the heading text to appear above the table
585 =cut
586 sub set_heading
587 {
588 my ($self, $heading) = @_;
589 $self->{'heading'} = $heading;
590 }
591
592 sub get_heading
593 {
594 my ($self) = @_;
595 return $self->{'heading'};
596 }
597
598 =head2 set_pagesize(pagesize)
599 Sets the size of a page. Set to 0 to turn off completely.
600 =cut
601 sub set_pagesize
602 {
603 my ($self, $pagesize) = @_;
604 $self->{'pagesize'} = $pagesize;
605 }
606
607 =head2 get_pagesize()
608 Returns the size of a page, or 0 if paging is turned off totally
609 =cut
610 sub get_pagesize
611 {
612 my ($self) = @_;
613 return $self->{'pagesize'};
614 }
615
616 sub get_pagepos
617 {
618 my ($self) = @_;
619 my $in = $self->{'form'} ? $self->{'form'}->{'in'} : undef;
620 my $name = $self->get_name();
621 if ($in && defined($in->{"ui_pagepos_".$name})) {
622         return ( $in->{"ui_pagepos_".$name} );
623         }
624 else {
625         return ( $self->{'pagepos'} );
626         }
627 }
628
629 =head2 make_url(sortcol, sortdir, paging, page, [no-searchargs], [no-pagearg])
630 Returns a link to this table's page, with the defaults for the various state
631 fields overriden by the parameters (where defined)
632 =cut
633 sub make_url
634 {
635 my ($self, $newsortcol, $newsortdir, $newpaging, $newpagepos,
636     $nosearch, $nopage) = @_;
637 my ($sortcol, $sortdir) = $self->get_sortcolumn();
638 $sortcol = $newsortcol if (defined($newsortcol));
639 $sortdir = $newsortdir if (defined($newsortdir));
640 my $paging = $self->get_paging();
641 $paging = $newpaging if (defined($newpaging));
642 my $pagepos = $self->get_pagepos();
643 $pagepos = $newpagepos if (defined($newpagepos));
644
645 my $thisurl = $self->{'form'}->{'page'}->get_myurl();
646 my $name = $self->get_name();
647 $thisurl .= $thisurl =~ /\?/ ? "&" : "?";
648 my $in = $self->{'form'}->{'in'};
649 return "${thisurl}ui_sortcolumn_${name}=$sortcol".
650        "&ui_sortdir_${name}=$sortdir".
651        "&ui_paging_${name}=$paging".
652        ($nopage ? "" : "&ui_pagepos_${name}=$pagepos").
653        ($nosearch ? "" : "&ui_searchfor_${name}=".
654                          &urlize($in->{"ui_searchfor_${name}"}).
655                          "&ui_searchcol_${name}=".
656                          &urlize($in->{"ui_searchcol_${name}"}));
657 }
658
659 1;
660