--- /dev/null
+use Webmin::Page;
+use Webmin::ResultPage;
+use Webmin::ErrorPage;
+use Webmin::ConfirmPage;
+use Webmin::Form;
+use Webmin::Section;
+use Webmin::Textbox;
+use Webmin::OptTextbox;
+use Webmin::OptTextarea;
+use Webmin::Submit;
+use Webmin::Password;
+use Webmin::Checkbox;
+use Webmin::Select;
+use Webmin::Radios;
+use Webmin::Checkboxes;
+use Webmin::Table;
+use Webmin::Menu;
+use Webmin::LinkTable;
+use Webmin::Tabs;
+use Webmin::Textarea;
+use Webmin::Upload;
+use Webmin::DynamicText;
+use Webmin::DynamicBar;
+use Webmin::DynamicWait;
+use Webmin::DynamicHTML;
+use Webmin::Properties;
+use Webmin::User;
+use Webmin::Group;
+use Webmin::File;
+use Webmin::Button;
+use Webmin::JavascriptButton;
+use Webmin::PlainText;
+use Webmin::Multiline;
+use Webmin::Date;
+use Webmin::Time;
+use Webmin::TitleList;
+use Webmin::Columns;
+use Webmin::Icon;
+use Webmin::TableAction;
+use Webmin::InputTable;
+use WebminCore;
+
+1;
+
--- /dev/null
+package Webmin::Button;
+use Webmin::Input;
+use WebminCore;
+@ISA = ( "Webmin::Input" );
+
+=head2 new Webmin::Button(cgi, label, [name])
+Creates a button that when clicked will link to some other page
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::Button::new) &&
+ caller() !~ /Webmin::Theme::Button/) {
+ return new Webmin::Theme::Button(@_[1..$#_]);
+ }
+my ($self, $cgi, $value, $name) = @_;
+$self = { };
+bless($self);
+$self->set_cgi($cgi);
+$self->set_value($value);
+$self->set_name($name) if ($name);
+return $self;
+}
+
+=head2 html()
+Returns HTML for this button
+=cut
+sub html
+{
+my ($self) = @_;
+my $rv = "<form action=".$self->get_cgi().">";
+foreach my $h (@{$self->{'hiddens'}}) {
+ $rv .= &ui_hidden($h->[0], $h->[1])."\n";
+ }
+$rv .= &ui_submit($self->get_value(), $self->get_name(),
+ $self->get_disabled())."</form>";
+return $rv;
+}
+
+sub set_cgi
+{
+my ($self, $cgi) = @_;
+$self->{'cgi'} = $cgi;
+}
+
+sub get_cgi
+{
+my ($self) = @_;
+return $self->{'cgi'};
+}
+
+=head2 add_hidden(name, value)
+Adds some hidden input to this button, for passing to the CGI
+=cut
+sub add_hidden
+{
+my ($self, $name, $value) = @_;
+push(@{$self->{'hiddens'}}, [ $name, $value ]);
+}
+
+1;
+
--- /dev/null
+package Webmin::Checkbox;
+use Webmin::Input;
+use WebminCore;
+@ISA = ( "Webmin::Input" );
+
+=head2 new Webmin::Checkbox(name, return, label, checked, [disabled])
+Create a single checkbox field
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::Checkbox::new)) {
+ return new Webmin::Theme::Checkbox(@_[1..$#_]);
+ }
+my ($self, $name, $return, $label, $checked, $disabled) = @_;
+$self = { };
+bless($self);
+$self->set_name($name);
+$self->set_return($return);
+$self->set_label($label);
+$self->set_value($checked);
+$self->set_disabled($disabled);
+return $self;
+}
+
+=head2 html()
+Returns the HTML for this single checkbox
+=cut
+sub html
+{
+my ($self) = @_;
+my $dis = $self->{'form'}->get_changefunc($self);
+return &ui_checkbox($self->get_name(), $self->get_return(),
+ $self->get_label(), $self->get_value(),
+ $dis ? "onClick='$dis'" : undef,
+ $self->get_disabled()).
+ &ui_hidden("ui_exists_".$self->get_name(), 1);
+}
+
+sub set_return
+{
+my ($self, $return) = @_;
+$self->{'return'} = $return;
+}
+
+sub set_label
+{
+my ($self, $label) = @_;
+$self->{'label'} = $label;
+}
+
+sub get_return
+{
+my ($self) = @_;
+return $self->{'return'};
+}
+
+sub get_label
+{
+my ($self) = @_;
+return $self->{'label'};
+}
+
+1;
+
--- /dev/null
+package Webmin::Checkboxes;
+use Webmin::Input;
+use WebminCore;
+@ISA = ( "Webmin::Input" );
+
+=head2 new Webmin::Checkboxes(name, value|&values, &options, [disabled])
+Create a list of checkboxes, of which zero or more may be selected
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::Checkboxes::new)) {
+ return new Webmin::Theme::Checkboxes(@_[1..$#_]);
+ }
+my ($self, $name, $value, $options, $disabled) = @_;
+$self = { };
+bless($self);
+$self->set_name($name);
+$self->set_value($value);
+$self->set_options($options);
+$self->set_disabled($disabled);
+return $self;
+}
+
+=head2 add_option(name, [label])
+=cut
+sub add_option
+{
+my ($self, $name, $label) = @_;
+push(@{$self->{'options'}}, [ $name, $label ]);
+}
+
+=head2 html()
+Returns the HTML for all the checkboxes, one after the other
+=cut
+sub html
+{
+my ($self) = @_;
+my $rv;
+for(my $i=0; $i<@{$self->{'options'}}; $i++) {
+ $rv .= $self->one_html($i)."\n";
+ }
+return $rv;
+}
+
+=head2 one_html(number)
+Returns the HTML for a single one of the checkboxes
+=cut
+sub one_html
+{
+my ($self, $num) = @_;
+my $opt = $self->{'options'}->[$num];
+my $value = $self->get_value();
+my %sel = map { $_, 1 } (ref($value) ? @$value : ( $value ));
+return &ui_checkbox($self->get_name(), $opt->[0],
+ defined($opt->[1]) ? $opt->[1] : $opt->[0],
+ $sel{$opt->[0]}, undef, $self->get_disabled()).
+ ($num == 0 ? &ui_hidden("ui_exists_".$self->get_name(), 1) : "");
+}
+
+=head2 get_value()
+Returns a hash ref of all selected values
+=cut
+sub get_value
+{
+my ($self) = @_;
+my $in = $self->{'form'} ? $self->{'form'}->{'in'} : undef;
+if ($in && (defined($in->{$self->{'name'}}) ||
+ defined($in->{"ui_exists_".$self->{'name'}}))) {
+ return [ split(/\0/, $in->{$self->{'name'}}) ];
+ }
+elsif ($in && defined($in->{"ui_value_".$self->{'name'}})) {
+ return [ split(/\0/, $in->{"ui_value_".$self->{'name'}}) ];
+ }
+else {
+ return $self->{'value'};
+ }
+}
+
+sub set_options
+{
+my ($self, $options) = @_;
+$self->{'options'} = $options;
+}
+
+sub get_options
+{
+my ($self) = @_;
+return $self->{'options'};
+}
+
+=head2 validate()
+Returns a list of error messages for this field
+=cut
+sub validate
+{
+my ($self) = @_;
+my $value = $self->get_value();
+if ($self->{'mandatory'} && !@$value) {
+ return ( $self->{'mandmesg'} || $text{'ui_checkmandatory'} );
+ }
+return ( );
+}
+
+=head2 get_input_names()
+Returns the actual names of all HTML elements that make up this input
+=cut
+sub get_input_names
+{
+my ($self) = @_;
+my @rv;
+for(my $i=0; $i<@{$self->{'options'}}; $i++) {
+ push(@rv, $self->{'name'}."[".$i."]");
+ }
+return @rv;
+}
+
+1;
+
+
--- /dev/null
+package Webmin::Columns;
+use WebminCore;
+
+=head2 new Webmin::Columns(cols)
+Displays some page elements in a multi-column table
+=cut
+sub new
+{
+my ($self, $cols) = @_;
+if (defined(&Webmin::Theme::Columns::new)) {
+ return new Webmin::Theme::Columns(@_[1..$#_]);
+ }
+$self = { 'columns' => 2 };
+bless($self);
+$self->set_columns($cols) if (defined($cols));
+return $self;
+}
+
+=head2 html()
+Returns HTML for the objects, arranged in columns
+=cut
+sub html
+{
+my ($self) = @_;
+my $rv;
+my $n = scalar(@{$self->{'contents'}});
+$rv .= "<table width=100% cellpadding=4><tr>\n";
+my $h = int($n / $self->{'columns'})+1;
+my $i = 0;
+my $pc = int(100/$self->{'columns'});
+foreach my $c (@{$self->{'contents'}}) {
+ if ($i%$h == 0) {
+ $rv .= "<td valign=top width=$pc%>";
+ }
+ $rv .= $c->html()."<p>\n";
+ $i++;
+ if ($i%$h == 0) {
+ $rv .= "</td>\n";
+ }
+ }
+$rv .= "</tr></table>\n";
+return $rv;
+}
+
+=head2 add(object)
+Adds some Webmin:: object to this list
+=cut
+sub add
+{
+my ($self, $object) = @_;
+push(@{$self->{'contents'}}, $object);
+if ($self->{'page'}) {
+ $object->set_page($self->{'page'});
+ }
+}
+
+sub set_columns
+{
+my ($self, $columns) = @_;
+$self->{'columns'} = $columns;
+}
+
+sub get_columns
+{
+my ($self) = @_;
+return $self->{'columns'};
+}
+
+=head2 set_page(Webmin::Page)
+Called when this menu is added to a page
+=cut
+sub set_page
+{
+my ($self, $page) = @_;
+$self->{'page'} = $page;
+foreach my $c (@{$self->{'contents'}}) {
+ $c->set_page($page);
+ }
+}
+
+1;
+
--- /dev/null
+package Webmin::ConfirmPage;
+use Webmin::Page;
+use WebminCore;
+@ISA = ( "Webmin::Page" );
+
+=head2 new Webmin::ConfirmPage(subheading, title, message, cgi, &in, [ok-message],
+ [cancel-message], [help-name])
+Create a new page object that asks if the user is sure if he wants to
+do something or not.
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::ConfirmPage::new)) {
+ return new Webmin::Theme::ConfirmPage(@_[1..$#_]);
+ }
+my ($self, $subheading, $title, $message, $cgi, $in, $ok, $cancel, $help) = @_;
+$self = new Webmin::Page($subheading, $title, $help);
+$self->{'in'} = $in;
+$self->add_message($message);
+my $form = new Webmin::Form($cgi, "get");
+$form->set_input($in);
+$self->add_form($form);
+foreach my $i (keys %$in) {
+ foreach my $v (split(/\0/, $in->{$i})) {
+ $form->add_hidden($i, $v);
+ }
+ }
+$form->add_button(new Webmin::Submit($ok || "OK", "ui_confirm"));
+$form->add_button(new Webmin::Submit($cancel || $text{'cancel'}, "ui_cancel"));
+bless($self);
+return $self;
+}
+
+sub get_confirm
+{
+my ($self) = @_;
+return $self->{'in'}->{'ui_confirm'} ? 1 : 0;
+}
+
+sub get_cancel
+{
+my ($self) = @_;
+return $self->{'in'}->{'ui_cancel'} ? 1 : 0;
+}
+
+1;
+
--- /dev/null
+package Webmin::Date;
+use Webmin::Input;
+use Time::Local;
+use WebminCore;
+@ISA = ( "Webmin::Input" );
+
+=head2 new Webmin::Date(name, time, [disabled])
+Create a new field for selecting a date
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::Date::new)) {
+ return new Webmin::Theme::Date(@_[1..$#_]);
+ }
+my ($self, $name, $value, $disabled) = @_;
+bless($self = { });
+$self->set_name($name);
+$self->set_value($value);
+$self->set_disabled($disabled) if (defined($disabled));
+return $self;
+}
+
+=head2 html()
+Returns the HTML for the date chooser
+=cut
+sub html
+{
+my ($self) = @_;
+my $rv;
+my @tm = localtime($self->get_value());
+my $name = $self->get_name();
+$rv .= &ui_date_input($tm[3], $tm[4]+1, $tm[5]+1900,
+ "day_".$name, "month_".$name, "year_".$name,
+ $self->get_disabled())." ".
+ &date_chooser_button("day_".$name, "month_".$name, "year_".$name);
+return $rv;
+}
+
+=head2 get_value()
+Returns the date as a Unix time number (for zero o'clock)
+=cut
+sub get_value
+{
+my ($self) = @_;
+my $in = $self->{'form'} ? $self->{'form'}->{'in'} : undef;
+if ($in && defined($in->{"day_".$self->{'name'}})) {
+ my $rv = $self->to_time($in);
+ return defined($rv) ? $rv : $self->{'value'};
+ }
+elsif ($in && defined($in->{"ui_value_".$self->{'name'}})) {
+ return $in->{"ui_value_".$self->{'name'}};
+ }
+else {
+ return $self->{'value'};
+ }
+}
+
+sub to_time
+{
+my ($self, $in) = @_;
+my $day = $in->{"day_".$self->{'name'}};
+return undef if ($day !~ /^\d+$/);
+my $month = $in->{"month_".$self->{'name'}}-1;
+my $year = $in->{"year_".$self->{'name'}}-1900;
+return undef if ($year !~ /^\d+$/);
+my $rv = eval { timelocal(0, 0, 0, $day, $month, $year) };
+return $@ ? undef : $rv;
+}
+
+sub set_validation_func
+{
+my ($self, $func) = @_;
+$self->{'validation_func'} = $func;
+}
+
+=head2 validate()
+Ensures that the date is valid
+=cut
+sub validate
+{
+my ($self) = @_;
+my $tm = $self->to_time($self->{'form'}->{'in'});
+if (!defined($tm)) {
+ return ( $text{'ui_edate'} );
+ }
+if ($self->{'validation_func'}) {
+ my $err = &{$self->{'validation_func'}}($self->get_value(),
+ $self->{'name'},
+ $self->{'form'});
+ return ( $err ) if ($err);
+ }
+return ( );
+}
+
+1;
+
--- /dev/null
+package Webmin::DynamicBar;
+use WebminCore;
+
+=head2 new Webmin::DynamicBar(&start-function, max)
+A page element for displaying progress towards some goal, like the download of
+a file.
+=cut
+sub new
+{
+my ($self, $func, $max) = @_;
+$self = { 'func' => $func,
+ 'name' => "dynamic".++$dynamic_count,
+ 'width' => 80,
+ 'max' => $max };
+bless($self);
+return $self;
+}
+
+=head2 set_message(text)
+Sets the text describing what we are waiting for
+=cut
+sub set_message
+{
+my ($self, $message) = @_;
+$self->{'message'} = $message;
+}
+
+sub get_message
+{
+my ($self) = @_;
+return $self->{'message'};
+}
+
+=head2 html()
+Returns the HTML for the text field
+=cut
+sub html
+{
+my ($self) = @_;
+my $rv;
+if ($self->get_message()) {
+ $rv .= $self->get_message()."<p>\n";
+ }
+$rv .= "<form name=form_$self->{'name'}>";
+$rv .= "<input name=bar_$self->{'name'} size=$self->{'width'} disabled=true style='font-family: courier'>";
+$rv .= " ";
+$rv .= "<input name=pc_$self->{'name'} size=3 disabled=true style='font-family: courier'>%";
+$rv .= "</form>";
+return $rv;
+}
+
+=head2 start()
+Called by the page to begin the progress
+=cut
+sub start
+{
+my ($self) = @_;
+&{$self->{'func'}}($self);
+}
+
+=head2 update(pos)
+Called by the function to update the position of the bar.
+=cut
+sub update
+{
+my ($self, $pos) = @_;
+my $pc = int(100*$pos/$self->{'max'});
+if ($pc != $self->{'lastpc'}) {
+ my $xn = int($self->{'width'}*$pos/$self->{'max'});
+ my $xes = "X" x $xn;
+ print "<script>window.document.forms[\"form_$self->{'name'}\"].pc_$self->{'name'}.value = \"$pc\";</script>\n";
+ print "<script>window.document.forms[\"form_$self->{'name'}\"].bar_$self->{'name'}.value = \"$xes\";</script>\n";
+ $self->{'lastpc'} = $pc;
+ }
+}
+
+=head2 set_wait(wait)
+If called with a non-zero arg, generation of the page should wait until this
+the progress is complete. Otherwise, the page will be generated completely before
+the start function is called
+=cut
+sub set_wait
+{
+my ($self, $wait) = @_;
+$self->{'wait'} = $wait;
+}
+
+sub get_wait
+{
+my ($self) = @_;
+return $self->{'wait'};
+}
+
+=head2 set_page(Webmin::Page)
+Called when this dynamic text box is added to a page
+=cut
+sub set_page
+{
+my ($self, $page) = @_;
+$self->{'page'} = $page;
+}
+
+sub set_width
+{
+my ($self, $width) = @_;
+$self->{'width'} = $width;
+}
+
+=head2 needs_unbuffered()
+Must return 1 if the page needs to be in un-buffered and no-table mode
+=cut
+sub needs_unbuffered
+{
+return 0;
+}
+
+
+1;
+
--- /dev/null
+package Webmin::DynamicHTML;
+use WebminCore;
+
+=head2 new Webmin::DynamicHTML(&function, &args, [before])
+When the page is being rendered, executes the given function and prints any
+text that it returns.
+=cut
+sub new
+{
+my ($self, $func, $args, $before) = @_;
+$self = { 'func' => $func,
+ 'args' => $args,
+ 'before' => $before };
+bless($self);
+return $self;
+}
+
+=head2 set_before(text)
+Sets the text describing what we are waiting for
+=cut
+sub set_before
+{
+my ($self, $before) = @_;
+$self->{'before'} = $before;
+}
+
+sub get_before
+{
+my ($self) = @_;
+return $self->{'before'};
+}
+
+sub html
+{
+my ($self) = @_;
+my $rv;
+if ($self->get_before()) {
+ $rv .= $self->get_before()."<p>\n";
+ }
+return $rv;
+}
+
+=head2 start()
+Called by the page to begin the dynamic output.
+=cut
+sub start
+{
+my ($self) = @_;
+&{$self->{'func'}}($self, @$args);
+}
+
+sub get_wait
+{
+my ($self) = @_;
+return 1;
+}
+
+=head2 needs_unbuffered()
+Must return 1 if the page needs to be in un-buffered and no-table mode
+=cut
+sub needs_unbuffered
+{
+return 1;
+}
+
+=head2 set_page(Webmin::Page)
+Called when this dynamic HTML element is added to a page
+=cut
+sub set_page
+{
+my ($self, $page) = @_;
+$self->{'page'} = $page;
+}
+
+1;
+
--- /dev/null
+# XXX should support non-Javascript mode?
+package Webmin::DynamicText;
+use WebminCore;
+
+=head2 new Webmin::DynamicText(&start-function, &args)
+A page element for displaying text that takes time to generate, such as from
+a long-running script. Uses a non-editable text box, updated via Javascript.
+The function will be called when it is time to start producing output, with this
+object as a parameter. It must call the add_line function on the object for each
+new line to be added.
+=cut
+sub new
+{
+my ($self, $func, $args) = @_;
+$self = { 'func' => $func,
+ 'args' => $args,
+ 'name' => "dynamic".++$dynamic_count,
+ 'rows' => 20,
+ 'cols' => 80 };
+bless($self);
+return $self;
+}
+
+=head2 set_message(text)
+Sets the text describing what we are waiting for
+=cut
+sub set_message
+{
+my ($self, $message) = @_;
+$self->{'message'} = $message;
+}
+
+sub get_message
+{
+my ($self) = @_;
+return $self->{'message'};
+}
+
+=head2 html()
+Returns the HTML for the text box
+=cut
+sub html
+{
+my ($self) = @_;
+my $rv;
+if ($self->get_message()) {
+ $rv .= $self->get_message()."<p>\n";
+ }
+$rv .= "<form name=form_$self->{'name'}>";
+$rv .= "<textarea name=$self->{'name'} rows=$self->{'rows'} cols=$self->{'cols'} wrap=off disabled=true>";
+$rv .= "</textarea>\n";
+$rv .= "</form>";
+return $rv;
+}
+
+=head2 start()
+Called by the page to begin the dynamic output.
+=cut
+sub start
+{
+my ($self) = @_;
+&{$self->{'func'}}($self, @$args);
+}
+
+=head2 add_line(line)
+Called by the function to add a line of text to this output
+=cut
+sub add_line
+{
+my ($self, $line) = @_;
+$line =~ s/\r|\n//g;
+$line = "e_escape($line);
+print "<script>window.document.forms[\"form_$self->{'name'}\"].$self->{'name'}.value += \"$line\"+\"\\n\";</script>\n";
+}
+
+=head2 set_wait(wait)
+If called with a non-zero arg, generation of the page should wait until this
+text box is complete. Otherwise, the page will be generated completely before the
+start function is called
+=cut
+sub set_wait
+{
+my ($self, $wait) = @_;
+$self->{'wait'} = $wait;
+}
+
+sub get_wait
+{
+my ($self) = @_;
+return $self->{'wait'};
+}
+
+=head2 set_page(Webmin::Page)
+Called when this dynamic text box is added to a page
+=cut
+sub set_page
+{
+my ($self, $page) = @_;
+$self->{'page'} = $page;
+}
+
+sub set_rows
+{
+my ($self, $rows) = @_;
+$self->{'rows'} = $rows;
+}
+
+sub set_cols
+{
+my ($self, $cols) = @_;
+$self->{'cols'} = $cols;
+}
+
+=head2 needs_unbuffered()
+Must return 1 if the page needs to be in un-buffered and no-table mode
+=cut
+sub needs_unbuffered
+{
+return 0;
+}
+
+1;
+
--- /dev/null
+package Webmin::DynamicWait;
+use WebminCore;
+
+=head2 new Webmin::DynamicWait(&start-function, [&args])
+A page element indicating that something is happening.
+=cut
+sub new
+{
+my ($self, $func, $args) = @_;
+$self = { 'func' => $func,
+ 'args' => $args,
+ 'name' => "dynamic".++$dynamic_count,
+ 'width' => 80,
+ 'delay' => 20 };
+bless($self);
+return $self;
+}
+
+=head2 set_message(text)
+Sets the text describing what we are waiting for
+=cut
+sub set_message
+{
+my ($self, $message) = @_;
+$self->{'message'} = $message;
+}
+
+sub get_message
+{
+my ($self) = @_;
+return $self->{'message'};
+}
+
+=head2 html()
+Returns the HTML for the text field used to indicate progress
+=cut
+sub html
+{
+my ($self) = @_;
+my $rv;
+if ($self->get_message()) {
+ $rv .= $self->get_message()."<p>\n";
+ }
+$rv .= "<form name=form_$self->{'name'}>";
+$rv .= "<input name=$self->{'name'} size=$self->{'width'} disabled=true style='font-family: courier'>";
+$rv .= "</form>";
+return $rv;
+}
+
+=head2 start()
+Called by the page to begin the progress. Also starts a process to update the
+Javascript text box
+=cut
+sub start
+{
+my ($self) = @_;
+$self->{'pid'} = fork();
+if (!$self->{'pid'}) {
+ my $pos = 0;
+ while(1) {
+ select(undef, undef, undef, $self->{'delay'}/1000.0);
+ my $str = (" " x $pos) . ("x" x 10);
+ print "<script>window.document.forms[\"form_$self->{'name'}\"].$self->{'name'}.value = \"$str\";</script>\n";
+ $pos++;
+ $pos = 0 if ($pos == $self->{'width'});
+ }
+ exit;
+ }
+&{$self->{'func'}}($self, @{$self->{'args'}});
+}
+
+=head2 stop()
+Called back by the function when whatever we were waiting for is done
+=cut
+sub stop
+{
+my ($self) = @_;
+if ($self->{'pid'}) {
+ kill('TERM', $self->{'pid'});
+ }
+my $str = (" " x ($self->{'width'}/2 - 2)) . "DONE";
+print "<script>window.document.forms[\"form_$self->{'name'}\"].$self->{'name'}.value = \"$str\";</script>\n";
+}
+
+=head2 set_wait(wait)
+If called with a non-zero arg, generation of the page should wait until this
+the progress is complete. Otherwise, the page will be generated completely before
+the start function is called
+=cut
+sub set_wait
+{
+my ($self, $wait) = @_;
+$self->{'wait'} = $wait;
+}
+
+sub get_wait
+{
+my ($self) = @_;
+return $self->{'wait'};
+}
+
+=head2 set_page(Webmin::Page)
+Called when this dynamic text box is added to a page
+=cut
+sub set_page
+{
+my ($self, $page) = @_;
+$self->{'page'} = $page;
+}
+
+sub set_width
+{
+my ($self, $width) = @_;
+$self->{'width'} = $width;
+}
+
+=head2 needs_unbuffered()
+Must return 1 if the page needs to be in un-buffered and no-table mode
+=cut
+sub needs_unbuffered
+{
+return 0;
+}
+
+
+
+
+1;
+
--- /dev/null
+package Webmin::ErrorPage;
+use WebminCore;
+
+=head2 new Webmin::ErrorPage(subheading, title, message, [program-output], [help-name])
+Create a new page object for showing an error of some kind
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::ErrorPage::new)) {
+ return new Webmin::Theme::ErrorPage(@_[1..$#_]);
+ }
+my ($self, $subheading, $title, $message, $output, $help) = @_;
+$self = new Webmin::Page($subheading, $title, $help);
+$self->add_message("<b>",$text{'error'}," : ",$message,"</b>");
+$self->add_message("<pre>",$output,"</pre>");
+return $self;
+}
+
+1;
+
--- /dev/null
+package Webmin::File;
+use Webmin::Textbox;
+use WebminCore;
+@ISA = ( "Webmin::Textbox" );
+
+=head2 new Webmin::File(name, value, size, [directory], [disabled])
+A text box for selecting a file
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::File::new)) {
+ return new Webmin::Theme::File(@_[1..$#_]);
+ }
+my ($self, $name, $value, $size, $directory, $disabled) = @_;
+$self = new Webmin::Textbox($name, $value, $size, $disabled);
+bless($self);
+$self->set_directory($directory);
+return $self;
+}
+
+=head2 html()
+Returns the HTML for this file input
+=cut
+sub html
+{
+my ($self) = @_;
+my $rv = Webmin::Textbox::html($self);
+my $name = $self->get_name();
+my $directory = $self->get_directory();
+my $add = 0;
+my $chroot = $self->get_chroot();
+$rv .= "<input type=button name=${name}_button onClick='ifield = form.$name; chooser = window.open(\"$gconfig{'webprefix'}/chooser.cgi?add=$add&type=$directory&chroot=$chroot&file=\"+escape(ifield.value), \"chooser\", \"toolbar=no,menubar=no,scrollbar=no,width=400,height=300\"); chooser.ifield = ifield; window.ifield = ifield' value=\"...\">\n";
+return $rv;
+}
+
+sub set_directory
+{
+my ($self, $directory) = @_;
+$self->{'directory'} = $directory;
+}
+
+sub get_directory
+{
+my ($self) = @_;
+return $self->{'directory'};
+}
+
+sub set_chroot
+{
+my ($self, $chroot) = @_;
+$self->{'chroot'} = $chroot;
+}
+
+sub get_chroot
+{
+my ($self) = @_;
+return $self->{'chroot'};
+}
+
+=head2 get_input_names()
+Returns the actual names of all HTML elements that make up this input
+=cut
+sub get_input_names
+{
+my ($self) = @_;
+return ( $self->{'name'}, $self->{'name'}."_button" );
+}
+
+1;
+
--- /dev/null
+package Webmin::Form;
+use WebminCore;
+
+=head2 new Webmin::Form(cgi, [method])
+Creates a new form, which submits to the given CGI
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::Form::new)) {
+ return new Webmin::Theme::Form(@_[1..$#_]);
+ }
+my ($self, $program, $method) = @_;
+$self = { 'method' => 'get',
+ 'name' => "form".++$form_count };
+bless($self);
+$self->set_program($program);
+$self->set_method($method) if ($method);
+return $self;
+}
+
+=head2 html()
+Returns the HTML that makes up this form
+=cut
+sub html
+{
+my ($self) = @_;
+my $rv;
+if ($self->get_align()) {
+ $rv .= "<div align=".$self->get_align().">\n";
+ }
+$rv .= $self->form_start();
+if ($self->get_heading()) {
+ if (defined(&ui_subheading)) {
+ $rv .= &ui_subheading($self->get_heading());
+ }
+ else {
+ $rv .= "<h3>".$self->get_heading()."</h3>\n";
+ }
+ }
+
+# Add the sections
+foreach my $h (@{$self->{'hiddens'}}) {
+ $rv .= &ui_hidden($h->[0], $h->[1])."\n";
+ }
+foreach my $s (@{$self->{'sections'}}) {
+ $rv .= $s->html();
+ }
+
+# Check if we have any inputs that need disabling
+my @dis = $self->list_disable_inputs();
+if (@dis) {
+ # Yes .. generate a function for them
+ $rv .= "<script>\n";
+ $rv .= "function ui_disable_".$self->{'name'}."(form) {\n";
+ foreach my $i (@dis) {
+ foreach my $n ($i->get_input_names()) {
+ $rv .= " form.".$n.".disabled = (".
+ $i->get_disable_code().");\n";
+ }
+ }
+ $rv .= "}\n";
+ $rv .= "</script>\n";
+ }
+
+# Add the buttons at the end of the form
+my @buttonargs;
+foreach my $b (@{$self->{'buttons'}}) {
+ if (ref($b)) {
+ # An array of inputs
+ my $ihtml = join(" ", map { $_->html() } @$b);
+ push(@buttonargs, $ihtml);
+ }
+ else {
+ # A spacer
+ push(@buttonargs, "");
+ }
+ }
+$rv .= &ui_form_end(\@buttonargs);
+
+if ($self->get_align()) {
+ $rv .= "</div>\n";
+ }
+
+# Call the Javascript disable function
+if (@dis) {
+ $rv .= "<script>\n";
+ $rv .= "ui_disable_".$self->{'name'}."(window.document.forms[\"$self->{'name'}\"]);\n";
+ $rv .= "</script>\n";
+ }
+
+return $rv;
+}
+
+sub form_start
+{
+my ($self) = @_;
+return "<form action='$self->{'program'}' ".
+ ($self->{'method'} eq "post" ? "method=post" :
+ $self->{'method'} eq "form-data" ?
+ "method=post enctype=multipart/form-data" :
+ "method=get")." name=$self->{'name'}>\n";
+}
+
+=head2 add_section(section)
+Adds a Webmin::Section object to this form
+=cut
+sub add_section
+{
+my ($self, $section) = @_;
+push(@{$self->{'sections'}}, $section);
+$section->set_form($self);
+}
+
+=head2 get_section(idx)
+=cut
+sub get_section
+{
+my ($self, $idx) = @_;
+return $self->{'sections'}->[$idx];
+}
+
+=head2 add_button(button, [beside, ...])
+Adds a Webmin::Submit object to this form, for display at the bottom
+=cut
+sub add_button
+{
+my ($self, $button, @beside) = @_;
+push(@{$self->{'buttons'}}, [ $button, @beside ]);
+}
+
+=head2 add_button_spacer()
+Adds a gap between buttons, for grouping
+=cut
+sub add_button_spacer
+{
+my ($self, $spacer) = @_;
+push(@{$self->{'buttons'}}, $spacer);
+}
+
+=head2 add_hidden(name, value)
+Adds some hidden input to this form, for passing to the CGI
+=cut
+sub add_hidden
+{
+my ($self, $name, $value) = @_;
+push(@{$self->{'hiddens'}}, [ $name, $value ]);
+}
+
+=head2 validate()
+Validates all form inputs, based on the current CGI input hash. Returns a list
+of errors, each of which is field name and error message.
+=cut
+sub validate
+{
+my ($self) = @_;
+my @errs;
+foreach my $s (@{$self->{'sections'}}) {
+ push(@errs, $s->validate($self->{'in'}));
+ }
+return @errs;
+}
+
+=head2 validate_redirect(page, [&extra-errors])
+Validates the form, and if any errors are found re-directs to the given page
+with the errors, so that they can be displayed.
+=cut
+sub validate_redirect
+{
+my ($self, $page, $extras) = @_;
+if ($self->{'in'}->{'ui_redirecting'}) {
+ # If this page is displayed as part of a redirect, no need to validate!
+ return;
+ }
+my @errs = $self->validate();
+push(@errs, @$extras);
+if (@errs) {
+ my (@errlist, @vallist);
+ foreach my $e (@errs) {
+ push(@errlist, &urlize("ui_error_".$e->[0])."=".
+ &urlize($e->[1]));
+ }
+ foreach my $i ($self->list_inputs()) {
+ my $v = $i->get_value();
+ my @vals = ref($v) ? @$v : ( $v );
+ @vals = ( undef ) if (!@vals);
+ foreach $v (@vals) {
+ push(@vallist,
+ &urlize("ui_value_".$i->get_name())."=".
+ &urlize($v));
+ }
+ }
+ foreach my $h (@{$self->{'hiddens'}}) {
+ push(@vallist,
+ &urlize($h->[0])."=".&urlize($h->[1]));
+ }
+ if ($page =~ /\?/) { $page .= "&"; }
+ else { $page .= "?"; }
+ &redirect($page.join("&", "ui_redirecting=1", @errlist, @vallist));
+ exit(0);
+ }
+}
+
+=head2 validate_error(whatfailed)
+Validates the form, and if any errors are found displays an error page.
+=cut
+sub validate_error
+{
+my ($self, $whatfailed) = @_;
+my @errs = $self->validate();
+&error_setup($whatfailed);
+if (@errs == 1) {
+ &error($errs[0]->[2] ? "$errs[0]->[2] : $errs[0]->[1]"
+ : $errs[0]->[1]);
+ }
+elsif (@errs > 1) {
+ my $msg = $text{'ui_errors'}."<br>";
+ foreach my $e (@errs) {
+ $msg .= $e->[2] ? "$e->[2] : $e->[1]<br>\n"
+ : "$e->[1]<br>\n";
+ }
+ &error($msg);
+ }
+}
+
+=head2 field_errors(name)
+Returns a list of error messages associated with the field of some name, from
+the input passed to set_input
+=cut
+sub field_errors
+{
+my ($self, $name) = @_;
+my @errs;
+my $in = $self->{'in'};
+foreach my $i (keys %$in) {
+ if ($i eq "ui_error_".$name) {
+ push(@errs, split(/\0/, $in->{$i}));
+ }
+ }
+return @errs;
+}
+
+=head2 set_input(&input)
+Passes the form input hash to this form object, for use by the validate
+functions and for displaying errors next to fields.
+=cut
+sub set_input
+{
+my ($self, $in) = @_;
+$self->{'in'} = $in;
+}
+
+=head2 get_value(input-name)
+Returns the value of the input with the given name.
+=cut
+sub get_value
+{
+my ($self, $name) = @_;
+foreach my $s (@{$self->{'sections'}}) {
+ my $rv = $s->get_value($name);
+ return $rv if (defined($rv));
+ }
+return $self->{'in'}->{$name};
+}
+
+=head2 get_input(name)
+Returns the input with the given name
+=cut
+sub get_input
+{
+my ($self, $name) = @_;
+foreach my $i ($self->list_inputs()) {
+ return $i if ($i->get_name() eq $name);
+ }
+return undef;
+}
+
+sub set_program
+{
+my ($self, $program) = @_;
+$self->{'program'} = $program;
+}
+
+sub set_method
+{
+my ($self, $method) = @_;
+$self->{'method'} = $method;
+}
+
+=head2 list_inputs()
+Returns all inputs in all form sections
+=cut
+sub list_inputs
+{
+my ($self) = @_;
+my @rv;
+foreach my $s (@{$self->{'sections'}}) {
+ push(@rv, $s->list_inputs());
+ }
+return @rv;
+}
+
+=head2 list_disable_inputs()
+Returns a list of inputs that have disable functions
+=cut
+sub list_disable_inputs
+{
+my ($self) = @_;
+my @dis;
+foreach my $i ($self->list_inputs()) {
+ push(@dis, $i) if ($i->get_disable_code());
+ }
+return @dis;
+}
+
+=head2 set_page(Webmin::Page)
+Called when this form is added to a page
+=cut
+sub set_page
+{
+my ($self, $page) = @_;
+$self->{'page'} = $page;
+}
+
+=head2 get_changefunc(&input)
+Called by some input, to return the Javascript that should be called when this
+input changes it's value.
+=cut
+sub get_changefunc
+{
+my ($self, $input) = @_;
+my @dis = $self->list_disable_inputs();
+if (@dis) {
+ return "ui_disable_".$self->{'name'}."(form)";
+ }
+return undef;
+}
+
+=head2 set_heading(text)
+Sets the heading to be displayed above the form
+=cut
+sub set_heading
+{
+my ($self, $heading) = @_;
+$self->{'heading'} = $heading;
+}
+
+sub get_heading
+{
+my ($self) = @_;
+return $self->{'heading'};
+}
+
+=head2 get_formno()
+Returns the index of this form on the page
+=cut
+sub get_formno
+{
+my ($self) = @_;
+my $n = 0;
+foreach my $f (@{$self->{'page'}->{'contents'}}) {
+ if ($f eq $self) {
+ return $n;
+ }
+ elsif (ref($f) =~ /Form/) {
+ $n++;
+ }
+ }
+return undef;
+}
+
+=head2 add_onload(code)
+Adds some Javascript code for inclusion in the onLoad tag
+=cut
+sub add_onload
+{
+my ($self, $code) = @_;
+push(@{$self->{'onloads'}}, $code);
+}
+
+=head2 add_script(code)
+Adds some Javascript code for putting in the <head> section
+=cut
+sub add_script
+{
+my ($self, $script) = @_;
+push(@{$self->{'scripts'}}, $script);
+}
+
+=head2 set_align(align)
+Sets the alignment on the page (left, center, right)
+=cut
+sub set_align
+{
+my ($self, $align) = @_;
+$self->{'align'} = $align;
+}
+
+sub get_align
+{
+my ($self) = @_;
+return $self->{'align'};
+}
+
+1;
+
--- /dev/null
+package Webmin::Group;
+use Webmin::Textbox;
+use WebminCore;
+@ISA = ( "Webmin::Textbox" );
+
+=head2 new Webmin::Group(name, value, [multiple], [disabled])
+A text box for entering or selecting one or many Unix groupnames
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::Group::new)) {
+ return new Webmin::Theme::Group(@_[1..$#_]);
+ }
+my ($self, $name, $value, $multiple, $disabled) = @_;
+$self = new Webmin::Textbox($name, $value, $multiple ? 40 : 15, $disabled);
+bless($self);
+$self->set_multiple($multiple);
+return $self;
+}
+
+=head2 html()
+Returns the HTML for this group input
+=cut
+sub html
+{
+my ($self) = @_;
+my $rv = Webmin::Textbox::html($self);
+my $name = $self->get_name();
+my $multiple = $self->get_multiple();
+local $w = $multiple ? 500 : 300;
+$rv .= " <input type=button name=${name}_button onClick='ifield = form.$name; chooser = window.open(\"$gconfig{'webprefix'}/group_chooser.cgi?multi=$multiple&group=\"+escape(ifield.value), \"chooser\", \"toolbar=no,menubar=no,scrollbars=yes,width=$w,height=200\"); chooser.ifield = ifield; window.ifield = ifield' value=\"...\">\n";
+return $rv;
+}
+
+sub set_multiple
+{
+my ($self, $multiple) = @_;
+$self->{'multiple'} = $multiple;
+}
+
+sub get_multiple
+{
+my ($self) = @_;
+return $self->{'multiple'};
+}
+
+=head2 get_input_names()
+Returns the actual names of all HTML elements that make up this input
+=cut
+sub get_input_names
+{
+my ($self) = @_;
+return ( $self->{'name'}, $self->{'name'}."_button" );
+}
+
+1;
+
--- /dev/null
+package Webmin::Icon;
+use WebminCore;
+
+=head2 Webmin::Icon(type, [message])
+This object generates an icon indicating some status. Possible types are :
+ok - OK
+critial - A serious problem
+major - A relatively serious problem
+minor - A small problem
+Can be used inside tables and property lists
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::Icon::new) && caller() !~ /Webmin::Theme::Icon/) {
+ return new Webmin::Theme::Icon(@_[1..$#_]);
+ }
+my ($self, $type, $message) = @_;
+$self = { };
+bless($self);
+$self->set_type($type);
+$self->set_message($message) if (defined($message));
+return $self;
+}
+
+=head2 html()
+Returns HTML for the icon
+=cut
+sub html
+{
+my ($self) = @_;
+my $rv;
+$rv .= "<img src=/images/".$self->get_type().".gif align=middle>";
+if ($self->get_message()) {
+ $rv .= " ".$self->get_message();
+ }
+return $rv;
+}
+
+sub set_type
+{
+my ($self, $type) = @_;
+$self->{'type'} = $type;
+}
+
+sub get_type
+{
+my ($self) = @_;
+return $self->{'type'};
+}
+
+sub set_message
+{
+my ($self, $message) = @_;
+$self->{'message'} = $message;
+}
+
+sub get_message
+{
+my ($self) = @_;
+return $self->{'message'};
+}
+
+1;
+
--- /dev/null
+package Webmin::Input;
+use WebminCore;
+
+sub set_form
+{
+my ($self, $form) = @_;
+$self->{'form'} = $form;
+}
+
+sub set_name
+{
+my ($self, $name) = @_;
+$self->{'name'} = $name;
+}
+
+sub get_name
+{
+my ($self) = @_;
+return $self->{'name'};
+}
+
+sub set_disabled
+{
+my ($self, $disabled) = @_;
+$self->{'disabled'} = $disabled;
+}
+
+sub get_disabled
+{
+my ($self) = @_;
+return $self->{'disabled'};
+}
+
+=head2 validate()
+No validation is done by default
+=cut
+sub validate
+{
+return ( );
+}
+
+sub set_value
+{
+my ($self, $value) = @_;
+$self->{'value'} = $value;
+}
+
+=head2 get_value()
+Returns the current value for this field as entered by the user, the value
+set when the form is re-displayed due to an error, or the initial value.
+=cut
+sub get_value
+{
+my ($self) = @_;
+my $in = $self->{'form'} ? $self->{'form'}->{'in'} : undef;
+if ($in && (defined($in->{$self->{'name'}}) ||
+ defined($in->{"ui_exists_".$self->{'name'}}))) {
+ return $in->{$self->{'name'}};
+ }
+elsif ($in && defined($in->{"ui_value_".$self->{'name'}})) {
+ return $in->{"ui_value_".$self->{'name'}};
+ }
+else {
+ return $self->{'value'};
+ }
+}
+
+=head2 set_disable_code(javascript)
+Must be provided with a Javascript expression that will return true when this
+input should be disabled. May refer to other fields, via the variable 'form'.
+ie. form.mode.value = "0"
+Will be called every time any field's value changes.
+=cut
+sub set_disable_code
+{
+my ($self, $code) = @_;
+$self->{'disablecode'} = $code;
+}
+
+sub get_disable_code
+{
+my ($self) = @_;
+return $self->{'disablecode'};
+}
+
+=head2 get_input_names()
+Returns the actual names of all HTML elements that make up this input
+=cut
+sub get_input_names
+{
+my ($self) = @_;
+return ( $self->{'name'} );
+}
+
+=head2 set_label(text)
+Sets HTML to be displayed before this field
+=cut
+sub set_label
+{
+my ($self, $label) = @_;
+$self->{'label'} = $label;
+}
+
+sub get_label
+{
+my ($self) = @_;
+return $self->{'label'};
+}
+
+sub set_mandatory
+{
+my ($self, $mandatory, $mandmesg) = @_;
+$self->{'mandatory'} = $mandatory;
+$self->{'mandmesg'} = $mandmesg if (defined($mandmesg));
+}
+
+sub get_mandatory
+{
+my ($self) = @_;
+return $self->{'mandatory'};
+}
+
+=head2 get_errors()
+Returns a list of errors associated with this field
+=cut
+sub get_errors
+{
+my ($self) = @_;
+return $self->{'form'} ? $self->{'form'}->field_errors($self->get_name())
+ : ( );
+}
+
+1;
+
--- /dev/null
+package Webmin::InputTable;
+use Webmin::Table;
+use WebminCore;
+@ISA = ( "Webmin::Table" );
+
+=head2 new Webmin::InputTable(&headings, [width], [name], [heading])
+A table containing multiple rows of inputs, each of which is the same
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::InputTable::new) &&
+ caller() !~ /Webmin::Theme::InputTable/) {
+ return new Webmin::Theme::InputTable(@_[1..$#_]);
+ }
+my $self = defined(&Webmin::Theme::Table::new) ? Webmin::Theme::Table::new(@_)
+ : Webmin::Table::new(@_);
+bless($self);
+$self->{'rowcount'} = 0;
+return $self;
+}
+
+=head2 set_inputs(&inputs)
+Sets the objects to be used for each row
+=cut
+sub set_inputs
+{
+my ($self, $classes) = @_;
+$self->{'classes'} = $classes;
+}
+
+=head2 add_values(&values)
+Adds a row of inputs, with the given values
+=cut
+sub add_values
+{
+my ($self, $values) = @_;
+my @row;
+for(my $i=0; $i<@$values; $i++) {
+ my $cls = $self->{'classes'}->[$i];
+ my $newin = { %$cls };
+ bless($newin, ref($cls));
+ $newin->set_value($values->[$i]);
+ $newin->set_name($newin->get_name()."_".$self->{'rowcount'});
+ $newin->set_form($self->{'form'}) if ($self->{'form'});
+ push(@row, $newin);
+ }
+$self->add_row(\@row);
+$self->{'rowcount'}++;
+}
+
+=head2 get_values(row)
+Returns the values of the inputs in the given row
+=cut
+sub get_values
+{
+my ($self, $row) = @_;
+my @rv;
+foreach my $i (@{$self->{'rows'}->[$row]}) {
+ if (ref($i) && $i->isa("Webmin::Input")) {
+ push(@rv, $i->get_value());
+ }
+ }
+return @rv;
+}
+
+=head2 list_inputs()
+=cut
+sub list_inputs
+{
+my ($self) = @_;
+my @rv = Webmin::Table::list_inputs($self);
+foreach my $r (@{$self->{'rows'}}) {
+ foreach my $i (@$r) {
+ if ($i && ref($i) && $i->isa("Webmin::Input")) {
+ push(@rv, $i);
+ }
+ }
+ }
+return @rv;
+}
+
+sub get_rowcount
+{
+my ($self) = @_;
+return $self->{'rowcount'};
+}
+
+=head2 validate()
+Validates all inputs, and returns a list of error messages
+=cut
+sub validate
+{
+my ($self) = @_;
+my $seli = $self->{'selectinput'};
+my @errs;
+if ($seli) {
+ push(@errs, map { [ $seli->get_name(), $_ ] } $seli->validate());
+ }
+foreach my $i (@{$self->{'inputs'}}) {
+ foreach my $e ($i->validate()) {
+ push(@errs, [ $i->get_name(), $e ]);
+ }
+ }
+my $k = 1;
+foreach my $r (@{$self->{'rows'}}) {
+ my $j = 0;
+ my $skip;
+ if (defined($self->{'control'})) {
+ if ($r->[$self->{'control'}]->get_value() eq "") {
+ $skip = 1;
+ }
+ }
+ foreach my $i (@$r) {
+ if ($i && ref($i) && $i->isa("Webmin::Input") && !$skip) {
+ my $label = &text('ui_rowlabel', $k, $self->{'headings'}->[$j]);
+ foreach my $e ($i->validate()) {
+ push(@errs, [ $i->get_name(), $label." ".$e ]);
+ }
+ }
+ $j++;
+ }
+ $k++;
+ }
+return @errs;
+}
+
+=head2 set_control(column)
+Sets the column for which an empty value means no validation should be done
+=cut
+sub set_control
+{
+my ($self, $control) = @_;
+$self->{'control'} = $control;
+}
+
+1;
+
--- /dev/null
+package Webmin::JavascriptButton;
+use Webmin::Input;
+use WebminCore;
+@ISA = ( "Webmin::Input" );
+
+=head2 new Webmin::JavascriptButton(label, script, [disabled])
+Create a button that runs some Javascript when clicked
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::JavascriptButton::new) &&
+ caller() !~ /Webmin::Theme::JavascriptButton/) {
+ return new Webmin::Theme::JavascriptButton(@_[1..$#_]);
+ }
+my ($self, $value, $script, $disabled) = @_;
+$self = { };
+bless($self);
+$self->set_value($value);
+$self->set_script($script);
+$self->set_disabled($disabled) if ($disabled);
+return $self;
+}
+
+=head2 html()
+Returns the HTML for this text input
+=cut
+sub html
+{
+my ($self) = @_;
+return "<input type=button value=\""."e_escape($self->get_value())."\" ".
+ "onClick=\"".$self->get_script()."\">";
+}
+
+sub set_script
+{
+my ($self, $script) = @_;
+$self->{'script'} = $script;
+}
+
+sub get_script
+{
+my ($self) = @_;
+return $self->{'script'};
+}
+
+1;
+
--- /dev/null
+package Webmin::LinkTable;
+use Webmin::Table;
+use WebminCore;
+
+=head2 new Webmin::LinkTable(heading, [columns], [width], [name])
+Creates a new table that just displays links, like in the Users and Groups module
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::LinkTable::new) &&
+ caller() !~ /Webmin::Theme::LinkTable/) {
+ return new Webmin::Theme::LinkTable(@_[1..$#_]);
+ }
+my ($self, $heading, $columns, $width, $name) = @_;
+$self = { 'sorter' => \&Webmin::Table::default_sorter,
+ 'columns' => 4,
+ 'sortable' => 1 };
+bless($self);
+$self->set_heading($heading);
+$self->set_name($name) if (defined($name));
+$self->set_width($width) if (defined($width));
+$self->set_columns($columns) if (defined($columns));
+return $self;
+}
+
+=head2 add_entry(name, link)
+Adds one item to appear in the table
+=cut
+sub add_entry
+{
+my ($self, $name, $link) = @_;
+push(@{$self->{'entries'}}, [ $name, $link ]);
+}
+
+=head2 html()
+Returns the HTML for this table.
+=cut
+sub html
+{
+my ($self) = @_;
+
+# Prepare the selector
+my @srows = @{$self->{'entries'}};
+my %selmap;
+if (defined($self->{'selectinput'})) {
+ my $i = 0;
+ foreach my $r (@srows) {
+ $selmap{$r} = $self->{'selectinput'}->one_html($i);
+ $i++;
+ }
+ }
+
+# Sort the entries
+my $sortdir = $self->get_sortdir();
+if (defined($sortdir)) {
+ my $func = $self->{'sorter'};
+ @srows = sort { my $so = &$func($a->[0], $b->[0]);
+ $sortdir ? -$so : $so } @srows;
+ }
+
+# Build the sorter
+my $head;
+my $thisurl = $self->{'form'}->{'page'}->get_myurl();
+$thisurl .= $thisurl =~ /\?/ ? "&" : "?";
+my $name = $self->get_name();
+if ($self->get_sortable()) {
+ $head = "<table cellpadding=0 cellspacing=0 width=100%><tr>";
+ $head .= "<td><b>".$self->get_heading()."</b></td> <td align=right>";
+ if (!defined($sortdir)) {
+ # Not sorting .. show grey button
+ $head .= "<a href='${thisurl}ui_sortdir_${name}=0'>".
+ "<img src=/images/nosort.gif border=0></a>";
+ }
+ else {
+ # Sorting .. show button to switch mode
+ my $notsort = !$sortdir;
+ $head .= "<a href='${thisurl}ui_sortdir_${name}=$notsort'>".
+ "<img src=/images/sort.gif border=0></a>";
+ }
+ $head .= "</td></tr></table>";
+ }
+else {
+ $head = $self->get_heading();
+ }
+
+# Find any errors
+my $rv;
+if ($self->{'selectinput'}) {
+ # Get any errors for inputs
+ my @errs = $self->{'form'}->field_errors(
+ $self->{'selectinput'}->get_name());
+ if (@errs) {
+ foreach my $e (@errs) {
+ $rv .= "<font color=#ff0000>$e</font><br>\n";
+ }
+ }
+ }
+
+# Create the actual table
+$rv .= &ui_table_start($head,
+ defined($self->{'width'}) ? "width=$self->{'width'}"
+ : undef, 1);
+$rv .= "<td colspan=2><table width=100%>";
+my $i = 0;
+my $cols = $self->get_columns();
+my $pc = 100/$cols;
+foreach my $r (@srows) {
+ $rv .= "<tr>\n" if ($i%$cols == 0);
+ $rv .= "<td width=$pc%>".$selmap{$r}."<a href='$r->[1]'>".
+ &html_escape($r->[0])."</a></td>\n";
+ $rv .= "<tr>\n" if ($i%$cols == $cols-1);
+ $i++;
+ }
+if ($i%$cols) {
+ # Finish off row
+ while($i++%$cols != $cols-1) {
+ $rv .= "<td width=$pc%></td>\n";
+ }
+ $rv .= "</tr>\n";
+ }
+$rv .= "</table></td>";
+$rv .= &ui_table_end();
+return $rv;
+}
+
+=head2 set_heading(text)
+Sets the heading text to appear above the table
+=cut
+sub set_heading
+{
+my ($self, $heading) = @_;
+$self->{'heading'} = $heading;
+}
+
+sub get_heading
+{
+my ($self) = @_;
+return $self->{'heading'};
+}
+
+sub set_name
+{
+my ($self, $name) = @_;
+$self->{'name'} = $name;
+}
+
+=head2 get_name()
+Returns the name for indentifying this table in HTML
+=cut
+sub get_name
+{
+my ($self) = @_;
+if (defined($self->{'name'})) {
+ return $self->{'name'};
+ }
+elsif ($self->{'form'}) {
+ my $secs = $self->{'form'}->{'sections'};
+ for(my $i=0; $i<@$secs; $i++) {
+ return "table".$i if ($secs->[$i] eq $self);
+ }
+ }
+return "table";
+}
+
+=head2 set_sorter(function)
+Sets a function used for sorting fields. Will be called with two values to compare
+=cut
+sub set_sorter
+{
+my ($self, $func) = @_;
+$self->{'sorter'} = $func;
+}
+
+=head2 default_sorter(value1, value2)
+=cut
+sub default_sorter
+{
+my ($value1, $value2, $col) = @_;
+return lc($value1) cmp lc($value2);
+}
+
+=head2 set_sortable(sortable?)
+Tells the table if sorting is allowed or not. By default, it is.
+=cut
+sub set_sortable
+{
+my ($self, $sortable) = @_;
+$self->{'sortable'} = $sortable;
+}
+
+sub get_sortable
+{
+my ($self) = @_;
+return $self->{'sortable'};
+}
+
+=head2 get_sortdir()
+Returns the order to sort in (1 for descending)
+=cut
+sub get_sortdir
+{
+my ($self) = @_;
+my $in = $self->{'form'} ? $self->{'form'}->{'in'} : undef;
+my $name = $self->get_name();
+if ($in && defined($in->{"ui_sortdir_".$name})) {
+ return ( $in->{"ui_sortdir_".$name} );
+ }
+else {
+ return ( $self->{'sortdir'} );
+ }
+}
+
+=head2 set_sortdir(descending?)
+Sets the default sort direction, unless overridden by the user.
+=cut
+sub set_sortcolumn
+{
+my ($self, $desc) = @_;
+$self->{'sortdir'} = $desc;
+}
+
+=head2 set_width([number|number%])
+Sets the width of this entire table. Can be called with 100%, 500 or undef to use
+the minimum possible width.
+=cut
+sub set_width
+{
+my ($self, $width) = @_;
+$self->{'width'} = $width;
+}
+
+=head2 set_columns(cols)
+Sets the number of columns to display
+=cut
+sub set_columns
+{
+my ($self, $columns) = @_;
+$self->{'columns'} = $columns;
+}
+
+sub get_columns
+{
+my ($self) = @_;
+return $self->{'columns'};
+}
+
+=head2 set_form(form)
+Called by the Webmin::Form object when this table is added to it
+=cut
+sub set_form
+{
+my ($self, $form) = @_;
+$self->{'form'} = $form;
+if ($self->{'selectinput'}) {
+ $self->{'selectinput'}->set_form($form);
+ }
+}
+
+=head2 set_selector(input)
+Takes a Webmin::Checkboxes or Webmin::Radios object, and uses it to add checkboxes
+to all the entries
+=cut
+sub set_selector
+{
+my ($self, $input) = @_;
+$self->{'selectinput'} = $input;
+$input->set_form($form);
+}
+
+=head2 get_selector()
+Returns the UI element used for selecting rows
+=cut
+sub get_selector
+{
+my ($self) = @_;
+return $self->{'selectinput'};
+}
+
+=head2 validate()
+Validates the selector input
+=cut
+sub validate
+{
+my ($self) = @_;
+my $seli = $self->{'selectinput'};
+if ($seli) {
+ return map { [ $seli->get_name(), $_ ] } $seli->validate();
+ }
+return ( );
+}
+
+=head2 get_value(input-name)
+Returns the value of the input with the given name.
+=cut
+sub get_value
+{
+my ($self, $name) = @_;
+if ($self->{'selectinput'} && $self->{'selectinput'}->get_name() eq $name) {
+ return $self->{'selectinput'}->get_value();
+ }
+return undef;
+}
+
+=head2 list_inputs()
+Returns all inputs in all form sections
+=cut
+sub list_inputs
+{
+my ($self) = @_;
+return $self->{'selectinput'} ? ( $self->{'selectinput'} ) : ( );
+}
+
+1;
+
--- /dev/null
+package Webmin::Menu;
+use WebminCore;
+
+=head2 new Webmin::Menu(&options, [columns])
+Generates a menu of options, typically using icons.
+=cut
+sub new
+{
+my ($self, $options, $columns) = @_;
+if (defined(&Webmin::Theme::Menu::new)) {
+ return new Webmin::Theme::Menu(@_[1..$#_]);
+ }
+$self = { 'columns' => 4 };
+bless($self);
+$self->set_options($options);
+$self->set_columns($columns) if (defined($columns));
+return $self;
+}
+
+=head2 html()
+Returns the HTML for the table
+=cut
+sub html
+{
+my ($self) = @_;
+my (@links, @titles, @icons, @hrefs);
+foreach my $o (@{$self->{'options'}}) {
+ push(@links, $o->{'link'});
+ if ($o->{'link2'}) {
+ push(@titles, "$o->{'title'}</a> <a href='$o->{'link2'}'>$o->{'title2'}");
+ }
+ else {
+ push(@titles, $o->{'title'});
+ }
+ push(@icons, $o->{'icon'});
+ push(@hrefs, $o->{'href'});
+ }
+my $rv = &capture_function_output(\&icons_table,
+ \@links, \@titles, \@icons, $self->get_columns(),
+ \@hrefs);
+return $rv;
+}
+
+=head2 add_option(&option)
+=cut
+sub add_option
+{
+my ($self, $option) = @_;
+push(@{$self->{'options'}}, $option);
+}
+
+sub set_options
+{
+my ($self, $options) = @_;
+$self->{'options'} = $options;
+}
+
+sub get_options
+{
+my ($self) = @_;
+return $self->{'options'};
+}
+
+sub set_columns
+{
+my ($self, $columns) = @_;
+$self->{'columns'} = $columns;
+}
+
+sub get_columns
+{
+my ($self) = @_;
+return $self->{'columns'};
+}
+
+=head2 set_page(Webmin::Page)
+Called when this menu is added to a page
+=cut
+sub set_page
+{
+my ($self, $page) = @_;
+$self->{'page'} = $page;
+}
+
+1;
+
--- /dev/null
+package Webmin::Multiline;
+use Webmin::Textarea;
+use WebminCore;
+@ISA = ( "Webmin::Textarea" );
+
+=head2 new Webmin::Multiline(name, &lines, rows, cols, [disabled])
+Create a new input for entering multiple text entries. By default, just uses
+a textbox
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::Multiline::new)) {
+ return new Webmin::Theme::Multiline(@_[1..$#_]);
+ }
+my ($self, $name, $lines, $rows, $cols, $wrap, $disabled) = @_;
+$self = new Webmin::Textarea($name, join("\n", @$lines), $rows, $cols, undef, $disabled);
+bless($self);
+return $self;
+}
+
+=head2 set_lines(&lines)
+Sets the lines to display
+=cut
+sub set_lines
+{
+my ($self, $lines) = @_;
+$self->set_value(join("\n", @$lines));
+}
+
+=head2 get_lines()
+Returns an array ref of lines to display
+=cut
+sub get_lines
+{
+my ($self) = @_;
+return [ split(/[\r|\n]+/, $self->get_value()) ];
+}
+
+1;
+
--- /dev/null
+package Webmin::OptTextarea;
+use Webmin::Textarea;
+use WebminCore;
+@ISA = ( "Webmin::Textarea" );
+
+=head2 new Webmin::OptTextarea(name, value, rows, cols, [default-msg], [other-msg])
+Create a text area whose value is optional.
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::OptTextarea::new)) {
+ return new Webmin::Theme::OptTextarea(@_[1..$#_]);
+ }
+my ($self, $name, $value, $rows, $cols, $default, $other) = @_;
+$self = new Webmin::Textarea($name, $value, $rows, $cols);
+bless($self);
+$self->set_default($default || $text{'default'});
+$self->set_other($other) if ($other);
+return $self;
+}
+
+=head2 html()
+Returns the HTML for this optional text input
+=cut
+sub html
+{
+my ($self) = @_;
+my $rv;
+my $name = $self->get_name();
+my $value = $self->get_value();
+my $dis = $self->get_disabled();
+my $rows = $self->get_rows();
+my $columns = $self->get_cols();
+my $dis1 = &js_disable_inputs([ $name ], [ ]);
+my $dis2 = &js_disable_inputs([ ], [ $name ]);
+my $opt1 = $self->get_default();
+my $opt2 = $self->get_other();
+$rv .= "<input type=radio name=\""."e_escape($name."_def")."\" ".
+ "value=1 ".($value ne '' ? "" : "checked").
+ ($dis ? " disabled=true" : "")." onClick='$dis1'> ".$opt1."\n";
+$rv .= "<input type=radio name=\""."e_escape($name."_def")."\" ".
+ "value=0 ".($value ne '' ? "checked" : "").
+ ($dis ? " disabled=true" : "")." onClick='$dis2'> ".$opt2."<br>\n";
+$rv .= "<textarea name=\""."e_escape($name)."\" ".
+ ($value eq "" || $dis ? " disabled=true" : "").
+ "rows=$rows columns=$columns>".&html_escape($value)."</textarea>\n";
+return $rv;
+
+}
+
+=head2 validate(&inputs)
+=cut
+sub validate
+{
+my ($self, $in) = @_;
+if (defined($self->get_value())) {
+ if ($self->get_value() eq "") {
+ return ( $text{'ui_nothing'} );
+ }
+ return Webmin::Textbox::validate($self);
+ }
+return ( );
+}
+
+sub set_default
+{
+my ($self, $default) = @_;
+$self->{'default'} = $default;
+}
+
+sub get_default
+{
+my ($self) = @_;
+return $self->{'default'};
+}
+
+sub set_other
+{
+my ($self, $other) = @_;
+$self->{'other'} = $other;
+}
+
+sub get_other
+{
+my ($self) = @_;
+return $self->{'other'};
+}
+
+=head2 get_value()
+Returns the specified initial value for this field, or the value set when the
+form is re-displayed due to an error.
+=cut
+sub get_value
+{
+my ($self) = @_;
+my $in = $self->{'form'} ? $self->{'form'}->{'in'} : undef;
+if ($in && (defined($in->{$self->{'name'}}) ||
+ defined($in->{$self->{'name'}.'_def'}))) {
+ return $in->{$self->{'name'}.'_def'} ? undef : $in->{$self->{'name'}};
+ }
+elsif ($in && defined($in->{"ui_value_".$self->{'name'}})) {
+ return $in->{"ui_value_".$self->{'name'}};
+ }
+else {
+ return $self->{'value'};
+ }
+}
+
+=head2 get_input_names()
+Returns the actual names of all HTML elements that make up this input
+=cut
+sub get_input_names
+{
+my ($self) = @_;
+return ( $self->{'name'}, $self->{'name'}."_def[0]",
+ $self->{'name'}."_def[1]" );
+}
+
+1;
+
--- /dev/null
+package Webmin::OptTextbox;
+use Webmin::Textbox;
+use WebminCore;
+@ISA = ( "Webmin::Textbox" );
+
+=head2 new Webmin::OptTextbox(name, value, size, [default-msg], [other-msg])
+Create a text field whose value is optional.
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::OptTextbox::new)) {
+ return new Webmin::Theme::OptTextbox(@_[1..$#_]);
+ }
+my ($self, $name, $value, $size, $default, $other) = @_;
+$self = new Webmin::Textbox($name, $value, $size);
+bless($self);
+$self->set_default($default || $text{'default'});
+$self->set_other($other) if ($other);
+return $self;
+}
+
+=head2 html()
+Returns the HTML for this optional text input
+=cut
+sub html
+{
+my ($self) = @_;
+return &ui_opt_textbox($self->get_name(), $self->get_value(),
+ $self->{'size'}, $self->{'default'},
+ $self->{'other'});
+}
+
+=head2 validate(&inputs)
+=cut
+sub validate
+{
+my ($self, $in) = @_;
+if (defined($self->get_value())) {
+ if ($self->get_value() eq "") {
+ return ( $text{'ui_nothing'} );
+ }
+ return Webmin::Textbox::validate($self);
+ }
+return ( );
+}
+
+sub set_default
+{
+my ($self, $default) = @_;
+$self->{'default'} = $default;
+}
+
+sub set_other
+{
+my ($self, $other) = @_;
+$self->{'other'} = $other;
+}
+
+=head2 get_value()
+Returns the specified initial value for this field, or the value set when the
+form is re-displayed due to an error.
+=cut
+sub get_value
+{
+my ($self) = @_;
+my $in = $self->{'form'} ? $self->{'form'}->{'in'} : undef;
+if ($in && (defined($in->{$self->{'name'}}) ||
+ defined($in->{$self->{'name'}.'_def'}))) {
+ return $in->{$self->{'name'}.'_def'} ? undef : $in->{$self->{'name'}};
+ }
+elsif ($in && defined($in->{"ui_value_".$self->{'name'}})) {
+ return $in->{"ui_value_".$self->{'name'}};
+ }
+else {
+ return $self->{'value'};
+ }
+}
+
+=head2 get_input_names()
+Returns the actual names of all HTML elements that make up this input
+=cut
+sub get_input_names
+{
+my ($self) = @_;
+return ( $self->{'name'}, $self->{'name'}."_def[0]",
+ $self->{'name'}."_def[1]" );
+}
+
+1;
+
--- /dev/null
+package Webmin::Page;
+use WebminCore;
+use WebminCore;
+
+=head2 new Webmin::Page(subheading, title, [help-name], [show-config],
+ [no-module-index], [no-webmin-index], [rightside],
+ [header], [body-tags], [below-text])
+Create a new page object, with the given heading and other details
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::Page::new) && caller() !~ /Webmin::Theme::Page/) {
+ return new Webmin::Theme::Page(@_[1..$#_]);
+ }
+my ($self, $subheading, $title, $help, $config, $noindex, $nowebmin, $right,
+ $header, $body, $below) = @_;
+$self = { 'index' => 1, 'webmin' => 1, 'image' => "" };
+bless($self);
+$self->set_subheading($subheading);
+$self->set_title($title);
+$self->set_help($help);
+$self->set_config($config);
+$self->set_index(!$noindex);
+$self->set_webmin(!$nowebmin);
+$self->set_right($right);
+$self->set_header($header);
+$self->set_body($body);
+$self->set_below($below);
+return $self;
+}
+
+=head2 print()
+Actually outputs this page
+=cut
+sub print
+{
+my ($self) = @_;
+my $rv;
+
+# Work out if we need buffering/table
+foreach my $c (@{$self->{'contents'}}) {
+ if (ref($c) =~ /Dynamic/) {
+ $| = 1;
+ if ($c->needs_unbuffered()) {
+ $self->{'unbuffered'} = 1;
+ }
+ }
+ }
+
+# Show the page header
+my $func = $self->{'unbuffered'} ? \&ui_print_unbuffered_header
+ : \&ui_print_header;
+my $scripts;
+foreach my $s (@{$self->{'scripts'}},
+ (map { @{$_->{'scripts'}} } @{$self->{'contents'}})) {
+ $scripts .= "<script>\n".$s."\n</script>\n";
+ }
+my $onload;
+my @onloads = ( @{$self->{'onloads'}},
+ map { @{$_->{'onloads'}} } @{$self->{'contents'}} );
+if (@onloads) {
+ $onload = "onLoad='".join(" ", @onloads)."'";
+ }
+my @args = ( $self->{'subheading'}, $self->{'title'}, $self->{'image'},
+ $self->{'help'}, $self->{'config'}, $self->{'index'} ? undef : 1,
+ $self->{'webmin'} ? undef : 1, $self->{'right'},
+ $self->{'header'}.$scripts, $self->{'body'}.$onload,
+ $self->{'below'} );
+while(!defined($args[$#args])) {
+ pop(@args);
+ }
+if ($self->get_refresh()) {
+ print "Refresh: ",$self->get_refresh(),"\r\n";
+ }
+&ui_print_header(@args);
+
+# Add the tab top
+if ($self->{'tabs'}) {
+ print $self->{'tabs'}->top_html();
+ }
+
+# Add any pre-content stuff
+print $self->pre_content();
+
+if ($self->{'errormsg'}) {
+ # Show the error only
+ print $self->get_errormsg_html();
+ }
+else {
+ # Generate the forms and other stuff
+ foreach my $c (@{$self->{'contents'}}) {
+ if (!ref($c)) {
+ # Just a message
+ print "$c<p>\n";
+ }
+ else {
+ # Convert to HTML
+ eval { print $c->html(); };
+ if ($@) {
+ print "<pre>$@</pre>";
+ }
+ if (ref($c) =~ /Dynamic/ && $c->get_wait()) {
+ # Dynamic object .. execute now
+ $c->start();
+ }
+ }
+ }
+
+ # Generate buttons row
+ if ($self->{'buttons'}) {
+ print "<hr>\n";
+ print &ui_buttons_start();
+ foreach my $b (@{$self->{'buttons'}}) {
+ print &ui_buttons_row(@$b);
+ }
+ print &ui_buttons_end();
+ }
+ }
+
+# Add any post-content stuff
+print $self->post_content();
+
+# End of the tabs
+if ($self->{'tabs'}) {
+ print $self->{'tabs'}->bottom_html();
+ }
+
+# Print the footer
+my @footerargs;
+foreach my $f (@{$self->{'footers'}}) {
+ push(@footerargs, $f->[0], $f->[1]);
+ }
+&ui_print_footer(@footerargs);
+
+# Start any dynamic objects
+foreach my $c (@{$self->{'contents'}}) {
+ if (ref($c) =~ /Dynamic/ && !$c->get_wait()) {
+ $c->start();
+ }
+ }
+}
+
+=head2 add_footer(link, title)
+Adds a return link, typically for display at the end of the page.
+=cut
+sub add_footer
+{
+my ($self, $link, $title) = @_;
+push(@{$self->{'footers'}}, [ $link, $title ]);
+}
+
+=head2 get_footer(index)
+Returns the link for the numbered footer
+=cut
+sub get_footer
+{
+my ($self, $num) = @_;
+return $self->{'footers'}->[$num]->[0];
+}
+
+=head2 add_message(text, ...)
+Adds a text message, to appear at this point on the page
+=cut
+sub add_message
+{
+my ($self, @message) = @_;
+push(@{$self->{'contents'}}, join("", @message));
+}
+
+=head2 add_error(text, [command-output])
+Adds a an error message, possible accompanied by the command output
+=cut
+sub add_error
+{
+my ($self, $message, $out) = @_;
+$message = "<font color=#ff0000>$message</font>";
+if ($out) {
+ $message .= "<pre>$out</pre>";
+ }
+push(@{$self->{'contents'}}, $message);
+}
+
+=head2 add_message_after(&object, text, ...)
+Adds a message after some existing object
+=cut
+sub add_message_after
+{
+my ($self, $object, @message) = @_;
+splice(@{$self->{'contents'}}, $self->position_of($object)+1, 0,
+ join("", @message));
+}
+
+=head2 add_error_after(&object, text, [command-output])
+Adds an error message after some existing object
+=cut
+sub add_error_after
+{
+my ($self, $object, $message, $out) = @_;
+$message = "<font color=#ff0000>$message</font>";
+if ($out) {
+ $message .= "<pre>$out</pre>";
+ }
+splice(@{$self->{'contents'}}, $self->position_of($object)+1, 0,
+ $message);
+}
+
+sub position_of
+{
+my ($self, $object) = @_;
+for(my $i=0; $i<@{$self->{'contents'}}; $i++) {
+ if ($self->{'contents'}->[$i] eq $object) {
+ return $i;
+ }
+ }
+print STDERR "Could not find $object in ",join(" ",@{$self->{'contents'}}),"\n";
+return scalar(@{$self->{'contents'}});
+}
+
+=head2 add_form(Webmin::Form)
+Adds a form to be displayed on this page
+=cut
+sub add_form
+{
+my ($self, $form) = @_;
+push(@{$self->{'contents'}}, $form);
+$form->set_page($self);
+}
+
+=head2 add_separator()
+Adds some kind of separation between parts of this page, like an <hr>
+=cut
+sub add_separator
+{
+my ($self, $message) = @_;
+push(@{$self->{'contents'}}, "<hr>");
+}
+
+=head2 add_button(cgi, label, description, [&hiddens], [before-button],
+ [after-button])
+Adds an action button associated with this page, typically for display at the end
+=cut
+sub add_button
+{
+my ($self, $cgi, $label, $desc, $hiddens, $before, $after) = @_;
+push(@{$self->{'buttons'}}, [ $cgi, $label, $desc, join(" ", @$hiddens),
+ $before, $after ]);
+}
+
+=head2 add_tabs(Webmin::Tags)
+Tells the page to display the given set of tabs at the top
+=cut
+sub add_tabs
+{
+my ($self, $tabs) = @_;
+$self->{'tabs'} = $tabs;
+}
+
+=head2 add_dynamic(Webmin::DynamicText|Webmin::DynamicProgress)
+Adds an object that is dynamically generated, such as a text box or progress bar.
+=cut
+sub add_dynamic
+{
+my ($self, $dyn) = @_;
+push(@{$self->{'contents'}}, $dyn);
+$dyn->set_page($self);
+}
+
+sub set_subheading
+{
+my ($self, $subheading) = @_;
+$self->{'subheading'} = $subheading;
+}
+
+sub set_title
+{
+my ($self, $title) = @_;
+$self->{'title'} = $title;
+}
+
+sub set_help
+{
+my ($self, $help) = @_;
+$self->{'help'} = $help;
+}
+
+sub set_config
+{
+my ($self, $config) = @_;
+$self->{'config'} = $config;
+}
+
+sub set_index
+{
+my ($self, $index) = @_;
+$self->{'index'} = $index;
+}
+
+sub set_webmin
+{
+my ($self, $webmin) = @_;
+$self->{'webmin'} = $webmin;
+}
+
+sub set_right
+{
+my ($self, $right) = @_;
+$self->{'right'} = $right;
+}
+
+sub set_header
+{
+my ($self, $header) = @_;
+$self->{'header'} = $header;
+}
+
+sub set_body
+{
+my ($self, $body) = @_;
+$self->{'body'} = $body;
+}
+
+sub set_below
+{
+my ($self, $below) = @_;
+$self->{'below'} = $below;
+}
+
+sub set_unbuffered
+{
+my ($self, $unbuffered) = @_;
+$self->{'unbuffered'} = $unbuffered;
+}
+
+=head2 set_popup(popup?)
+If set to 1, then this is a popup window
+=cut
+sub set_popup
+{
+my ($self, $popup) = @_;
+$self->{'popup'} = $popup;
+}
+
+=head2 get_myurl()
+Returns the path part of the URL for this page, like /foo/bar.cgi
+=cut
+sub get_myurl
+{
+my @args;
+if ($ENV{'QUERY_STRING'} && $ENV{'REQUEST_METHOD'} ne 'POST') {
+ my %in;
+ &ReadParse(\%in);
+ foreach my $i (keys %in) {
+ if ($i !~ /^ui_/) {
+ foreach my $v (split(/\0/, $in{$i})) {
+ push(@args, &urlize($i)."=".
+ &urlize($v));
+ }
+ }
+ }
+ }
+return @args ? $ENV{'SCRIPT_NAME'}."?".join("&", @args)
+ : $ENV{'SCRIPT_NAME'};
+}
+
+=head2 set_refresh(seconds)
+Sets the number of seconds between automatic page refreshes
+=cut
+sub set_refresh
+{
+my ($self, $refresh) = @_;
+$self->{'refresh'} = $refresh;
+}
+
+sub get_refresh
+{
+my ($self) = @_;
+return $self->{'refresh'};
+}
+
+=head2 add_onload(code)
+Adds some Javascript code for inclusion in the onLoad tag
+=cut
+sub add_onload
+{
+my ($self, $code) = @_;
+push(@{$self->{'onloads'}}, $code);
+}
+
+=head2 add_script(code)
+Adds some Javascript code for putting in the <head> section
+=cut
+sub add_script
+{
+my ($self, $script) = @_;
+push(@{$self->{'scripts'}}, $script);
+}
+
+sub pre_content
+{
+my ($self) = @_;
+return undef;
+}
+
+sub post_content
+{
+my ($self) = @_;
+return undef;
+}
+
+=head2 set_errormsg(message)
+Sets an error message to be displayed instead of the page contents
+=cut
+sub set_errormsg
+{
+my ($self, $errormsg) = @_;
+$self->{'errormsg'} = $errormsg;
+}
+
+sub get_errormsg_html
+{
+my ($self) = @_;
+return $self->{'errormsg'}."<p>\n";
+}
+
+1;
+
--- /dev/null
+package Webmin::Password;
+@ISA = ( "Webmin::Textbox" );
+use Webmin::Textbox;
+use WebminCore;
+
+=head2 new Webmin::Password(name, value, [size])
+Create a new text input field, for a password
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::Password::new)) {
+ return new Webmin::Theme::Password(@_[1..$#_]);
+ }
+my ($self, $name, $value, $size) = @_;
+$self = new Webmin::Textbox($name, $value, $size);
+bless($self);
+return $self;
+}
+
+=head2 html()
+Returns the HTML for this password input
+=cut
+sub html
+{
+my ($self) = @_;
+return &ui_password($self->get_name(), $self->get_value(),
+ $self->{'size'},
+ $self->{'$disabled'});
+}
+
+
+
--- /dev/null
+package Webmin::PlainText;
+use WebminCore;
+
+=head2 new Webmin::PlainText(text, columns)
+Displays a block of plain fixed-width text, within a page or form.
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::PlainText::new) &&
+ caller() !~ /Webmin::Theme::PlainText/) {
+ return new Webmin::Theme::PlainText(@_[1..$#_]);
+ }
+my ($self, $text, $columns) = @_;
+$self = { 'columns' => 80 };
+bless($self);
+$self->set_text($text);
+$self->set_columns($columns) if (defined($columns));
+return $self;
+}
+
+=head2 html()
+=cut
+sub html
+{
+my ($self) = @_;
+my $rv;
+$rv .= "<table border><tr $cb><td><pre>";
+foreach my $l (&wrap_lines($self->get_text(), $self->get_columns())) {
+ if (length($l) < $self->get_columns()) {
+ $l .= (" " x $self->get_columns() - length($l));
+ }
+ $rv .= &html_escape($l)."\n";
+ }
+if (!$self->get_text()) {
+ print (" " x $self->get_columns()),"\n";
+ }
+$rv .= "</pre></td></tr></table>\n";
+return $rv;
+}
+
+sub set_text
+{
+my ($self, $text) = @_;
+$self->{'text'} = $text;
+}
+
+sub get_text
+{
+my ($self) = @_;
+return $self->{'text'};
+}
+
+sub set_columns
+{
+my ($self, $columns) = @_;
+$self->{'columns'} = $columns;
+}
+
+sub get_columns
+{
+my ($self) = @_;
+return $self->{'columns'};
+}
+
+# wrap_lines(text, width)
+# Given a multi-line string, return an array of lines wrapped to
+# the given width
+sub wrap_lines
+{
+local @rv;
+local $w = $_[1];
+foreach $rest (split(/\n/, $_[0])) {
+ if ($rest =~ /\S/) {
+ while($rest =~ /^(.{1,$w}\S*)\s*([\0-\377]*)$/) {
+ push(@rv, $1);
+ $rest = $2;
+ }
+ }
+ else {
+ # Empty line .. keep as it is
+ push(@rv, $rest);
+ }
+ }
+return @rv;
+}
+
+=head2 set_page(Webmin::Page)
+Called when this form is added to a page
+=cut
+sub set_page
+{
+my ($self, $page) = @_;
+$self->{'page'} = $page;
+}
+
+1;
+
--- /dev/null
+package Webmin::Properties;
+use WebminCore;
+
+=head2 new Webmin::Properties([heading], [columns], [width])
+Creates a read-only properties list
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::Properties::new) &&
+ caller() !~ /Webmin::Theme::Properties/) {
+ return new Webmin::Theme::Properties(@_[1..$#_]);
+ }
+my ($self, $heading, $columns, $width) = @_;
+$self = { 'columns' => 2 };
+bless($self);
+$self->set_heading($heading) if (defined($heading));
+$self->set_columns($columns) if (defined($columns));
+$self->set_width($width) if (defined($width));
+return $self;
+}
+
+=head2 add_row(label, value, ...)
+Adds one row to the properties table
+=cut
+sub add_row
+{
+my ($self, @row) = @_;
+push(@{$self->{'rows'}}, \@row);
+}
+
+=head2 set_heading_row(head1, head2, ...)
+Adds a row of headings
+=cut
+sub set_heading_row
+{
+my ($self, @row) = @_;
+$self->{'heading_row'} = \@row;
+}
+
+=head2 html()
+Returns the HTML for this properties list
+=cut
+sub html
+{
+my ($self) = @_;
+my $rv;
+my $width = $self->get_width();
+$rv .= "<table border ".($width ? "width=$width" : "").">\n";
+$rv .= "<tr><td><table width=100% cellspacing=0 cellpadding=3>\n";
+my $cols = $self->get_columns();
+if ($self->get_heading()) {
+ $rv .= "<tr $tb><td colspan=$cols><b>".
+ $self->get_heading()."</b></td> </tr>\n";
+ }
+if ($self->{'heading_row'}) {
+ $rv .= "<tr $tb>\n";
+ foreach my $r (@{$self->{'heading_row'}}) {
+ $rv .= "<td><b>$r</b></td>\n";
+ }
+ $rv .= "</tr>\n";
+ }
+foreach my $r (@{$self->{'rows'}}) {
+ $rv .= "<tr $cb>\n";
+ $rv .= "<td><b>$r->[0]</b></td>\n";
+ for(my $i=1; $i<@$r || $i<$cols; $i++) {
+ $rv .= "<td>".(ref($r->[$i]) ? $r->[$i]->html()
+ : $r->[$i])."</td>\n";
+ }
+ $rv .= "</tr>\n";
+ }
+$rv .= "</table></td></tr></table>\n";
+return $rv;
+}
+
+=head2 set_width([number|number%])
+Sets the width of this section. Can be called with 100%, 500, or undef to use
+the minimum possible width.
+=cut
+sub set_width
+{
+my ($self, $width) = @_;
+$self->{'width'} = $width;
+}
+
+sub get_width
+{
+my ($self) = @_;
+return $self->{'width'};
+}
+
+=head2 set_columns(number)
+Sets the number of columns in the properties table, including the title column
+=cut
+sub set_columns
+{
+my ($self, $columns) = @_;
+$self->{'columns'} = $columns;
+}
+
+sub get_columns
+{
+my ($self) = @_;
+return $self->{'columns'};
+}
+
+=head2 set_heading(number)
+Sets the heading to appear above the properties list
+=cut
+sub set_heading
+{
+my ($self, $heading) = @_;
+$self->{'heading'} = $heading;
+}
+
+sub get_heading
+{
+my ($self) = @_;
+return $self->{'heading'};
+}
+
+
+=head2 set_page(Webmin::Page)
+Called when this form is added to a page
+=cut
+sub set_page
+{
+my ($self, $page) = @_;
+$self->{'page'} = $page;
+}
+
+1;
+
--- /dev/null
+package Webmin::Radios;
+use Webmin::Input;
+use WebminCore;
+@ISA = ( "Webmin::Input" );
+
+=head2 new Webmin::Radios(name, value, &options, [disabled])
+Create a list of radio buttons, of which one may be selected
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::Radios::new)) {
+ return new Webmin::Theme::Radios(@_[1..$#_]);
+ }
+my ($self, $name, $value, $options, $disabled) = @_;
+$self = { };
+bless($self);
+$self->set_name($name);
+$self->set_value($value);
+$self->set_options($options);
+$self->set_disabled($disabled);
+return $self;
+}
+
+=head2 add_option(name, [label])
+=cut
+sub add_option
+{
+my ($self, $name, $label) = @_;
+push(@{$self->{'options'}}, [ $name, $label ]);
+}
+
+=head2 html()
+Returns the HTML for all the radio buttons, one after the other
+=cut
+sub html
+{
+my ($self) = @_;
+my $dis = $self->{'form'}->get_changefunc($self);
+my $opts = $self->get_options();
+if ($dis) {
+ foreach my $o (@$opts) {
+ $o->[2] = "onClick='$dis'";
+ }
+ }
+return &ui_radio($self->get_name(), $self->get_value(),
+ $opts, $self->get_disabled());
+}
+
+=head2 one_html(number)
+Returns the HTML for a single one of the radio buttons
+=cut
+sub one_html
+{
+my ($self, $num) = @_;
+my $opt = $self->{'options'}->[$num];
+my $dis = $self->{'form'}->get_changefunc($self);
+return &ui_oneradio($self->get_name(), $opt->[0],
+ defined($opt->[1]) ? $opt->[1] : $opt->[0],
+ $self->get_value() eq $opt->[0],
+ $dis ? "onClick='$dis'" : undef,
+ $self->get_disabled());
+}
+
+sub set_options
+{
+my ($self, $options) = @_;
+$self->{'options'} = $options;
+}
+
+sub get_options
+{
+my ($self) = @_;
+return $self->{'options'};
+}
+
+1;
+
--- /dev/null
+package Webmin::ResultPage;
+use WebminCore;
+
+=head2 new Webmin::ResultPage(subheading, title, message, [help-name])
+Create a new page object for showing some success message.
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::ResultPage::new) &&
+ caller() !~ /Webmin::Theme::ResultPage/) {
+ return new Webmin::Theme::ResultPage(@_[1..$#_]);
+ }
+my ($self, $subheading, $title, $message, $help) = @_;
+$self = new Webmin::Page($subheading, $title, $help);
+$self->add_message("<b>$message</b>");
+return $self;
+}
+
+1;
+
--- /dev/null
+package Webmin::Section;
+use WebminCore;
+
+=head2 new Webmin::Section(header, [columns], [title], [width])
+Create a new form section, which has a header and contains some inputs
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::Section::new) &&
+ caller() !~ /Webmin::Theme::Section/) {
+ return new Webmin::Theme::Section(@_[1..$#_]);
+ }
+my ($self, $header, $columns, $title, $width) = @_;
+$self = { 'columns' => 4 };
+bless($self);
+$self->set_header($header);
+$self->set_columns($columns) if (defined($columns));
+$self->set_title($title) if (defined($title));
+$self->set_width($width) if (defined($width));
+return $self;
+}
+
+=head2 html()
+Returns the HTML for this form section
+=cut
+sub html
+{
+my ($self) = @_;
+my $rv;
+$rv .= &ui_table_start($self->{'header'},
+ $self->{'width'} ? "width=$self->{'width'}" : undef,
+ $self->{'columns'});
+foreach my $i (@{$self->{'inputs'}}) {
+ if (is_input($i->[1])) {
+ my $errs;
+ my @errs = $self->{'form'}->field_errors($i->[1]->get_name());
+ if (@errs) {
+ foreach my $e (@errs) {
+ $errs .= "<br><font color=#ff0000>$e</font>\n";
+ }
+ }
+ $rv .= &ui_table_row($i->[0], $i->[1]->html().$errs,
+ $i->[2]);
+ }
+ else {
+ $rv .= &ui_table_row($i->[0],
+ ref($i->[1]) ? $i->[1]->html() : $i->[1], $i->[2]);
+ }
+ }
+$rv .= &ui_table_end();
+return $rv;
+}
+
+=head2 add_input(label, input, [columns])
+Adds some Webmin::Input object to this form section
+=cut
+sub add_input
+{
+my ($self, $label, $input, $cols) = @_;
+push(@{$self->{'inputs'}}, [ $label, $input, $cols ]);
+$input->set_form($self->{'form'});
+}
+
+=head2 add_row(label, text, [columns])
+Adds a non-editable row to this form section
+=cut
+sub add_row
+{
+my ($self, $label, $text, $cols) = @_;
+push(@{$self->{'inputs'}}, [ $label, $text, $cols ]);
+}
+
+=head2 add_separator()
+Adds some kind of separator at this point in the section
+=cut
+sub add_separator
+{
+my ($self) = @_;
+push(@{$self->{'inputs'}}, [ undef, "<hr>", $self->{'columns'} ]);
+}
+
+sub set_header
+{
+my ($self, $header) = @_;
+$self->{'header'} = $header;
+}
+
+sub set_columns
+{
+my ($self, $columns) = @_;
+$self->{'columns'} = $columns;
+}
+
+sub set_title
+{
+my ($self, $title) = @_;
+$self->{'title'} = $title;
+}
+
+=head2 set_width([number|number%])
+Sets the width of this section. Can be called with 100%, 500, or undef to use
+the minimum possible width.
+=cut
+sub set_width
+{
+my ($self, $width) = @_;
+$self->{'width'} = $width;
+}
+
+=head2 validate()
+Validates all form inputs, based on the given CGI input hash. Returns a list
+of errors, each of which is field name, error message and field label.
+=cut
+sub validate
+{
+my ($self) = @_;
+my @errs;
+foreach my $i (@{$self->{'inputs'}}) {
+ if (is_input($i->[1])) {
+ foreach my $e ($i->[1]->validate()) {
+ push(@errs, [ $i->[1]->get_name(), $e, $i->[0] ]);
+ }
+ }
+ }
+return @errs;
+}
+
+=head2 get_value(input-name)
+Returns the value of the input with the given name.
+=cut
+sub get_value
+{
+my ($self, $name) = @_;
+foreach my $i (@{$self->{'inputs'}}) {
+ if (is_input($i->[1]) && $i->[1]->get_name() eq $name) {
+ return $i->[1]->get_value();
+ }
+ }
+return undef;
+}
+
+=head2 set_form(form)
+Called by the Webmin::Form object when this section is added to it
+=cut
+sub set_form
+{
+my ($self, $form) = @_;
+$self->{'form'} = $form;
+foreach my $i (@{$self->{'inputs'}}) {
+ if (is_input($i->[1])) {
+ $i->[1]->set_form($form);
+ }
+ }
+}
+
+sub list_inputs
+{
+my ($self) = @_;
+return map { $_->[1] } grep { is_input($_->[1]) } @{$self->{'inputs'}};
+}
+
+=head2 is_input(object)
+=cut
+sub is_input
+{
+my ($object) = @_;
+return ref($object) && ref($object) =~ /::/ &&
+ $object->isa("Webmin::Input");
+}
+
+1;
+
--- /dev/null
+package Webmin::Select;
+use Webmin::Input;
+use WebminCore;
+@ISA = ( "Webmin::Input" );
+
+=head2 new Webmin::Select(name, value|&values, &options, [multiple-size],
+ [add-missing], [disabled])
+Create a menu or multiple-selection field
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::Select::new)) {
+ return new Webmin::Theme::Select(@_[1..$#_]);
+ }
+my ($self, $name, $value, $options, $size, $missing, $disabled) = @_;
+$self = { 'size' => 1 };
+bless($self);
+$self->set_name($name);
+$self->set_value($value);
+$self->set_options($options);
+$self->set_size($size) if (defined($size));
+$self->set_missing($missing);
+$self->set_disabled($disabled);
+return $self;
+}
+
+=head2 add_option(name, [label])
+=cut
+sub add_option
+{
+my ($self, $name, $label) = @_;
+push(@{$self->{'options'}}, [ $name, $label ]);
+}
+
+=head2 html()
+Returns the HTML for this menu or multi-select input
+=cut
+sub html
+{
+my ($self) = @_;
+my $dis = $self->{'form'}->get_changefunc($self);
+return &ui_select($self->get_name(), $self->get_value(),
+ $self->get_options(),
+ $self->get_size() > 1 ? $self->get_size() : undef,
+ $self->get_size() > 1 ? 1 : 0,
+ undef,
+ $self->get_disabled(),
+ $dis ? "onChange='$dis'" : undef).
+ ($self->get_size() > 1 ?
+ &ui_hidden("ui_exists_".$self->get_name(), 1) : "");
+}
+
+=head2 get_value()
+For a multi-select field, returns an array ref of all values. For a menu,
+return just the one value.
+=cut
+sub get_value
+{
+my ($self) = @_;
+my $in = $self->{'form'} ? $self->{'form'}->{'in'} : undef;
+if ($in && (defined($in->{$self->{'name'}}) ||
+ defined($in->{"ui_exists_".$self->{'name'}}))) {
+ if ($self->get_size() > 1) {
+ return [ split(/\0/, $in->{$self->{'name'}}) ];
+ }
+ else {
+ return $in->{$self->{'name'}};
+ }
+ }
+elsif ($in && defined($in->{"ui_value_".$self->{'name'}})) {
+ if ($self->get_size() > 1) {
+ return [ split(/\0/, $in->{"ui_value_".$self->{'name'}}) ];
+ }
+ else {
+ return $in->{"ui_value_".$self->{'name'}};
+ }
+ }
+else {
+ return $self->{'value'};
+ }
+}
+
+sub set_options
+{
+my ($self, $options) = @_;
+$self->{'options'} = $options;
+}
+
+sub set_size
+{
+my ($self, $size) = @_;
+$self->{'size'} = $size;
+}
+
+sub set_missing
+{
+my ($self, $missing) = @_;
+$self->{'missing'} = $missing;
+}
+
+sub get_options
+{
+my ($self) = @_;
+return $self->{'options'};
+}
+
+sub get_size
+{
+my ($self) = @_;
+return $self->{'size'};
+}
+
+sub get_missing
+{
+my ($self) = @_;
+return $self->{'missing'};
+}
+
+=head2 validate()
+Returns a list of error messages for this field
+=cut
+sub validate
+{
+my ($self) = @_;
+if ($self->{'size'} > 1) {
+ my $value = $self->get_value();
+ if ($self->{'mandatory'} && !@$value) {
+ return ( $self->{'mandatorymsg'} || $text{'ui_mandatory'} );
+ }
+ }
+return ( );
+}
+
+1;
+
--- /dev/null
+package Webmin::Submit;
+use Webmin::Input;
+use WebminCore;
+@ISA = ( "Webmin::Input" );
+
+=head2 new Webmin::Submit(label, [name], [disabled])
+Create a form submit button
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::Submit::new) &&
+ caller() !~ /Webmin::Theme::Submit/) {
+ return new Webmin::Theme::Submit(@_[1..$#_]);
+ }
+my ($self, $value, $name, $disabled) = @_;
+$self = { };
+bless($self);
+$self->set_value($value);
+$self->set_name($name) if ($name);
+$self->set_disabled($disabled) if ($disabled);
+return $self;
+}
+
+=head2 html()
+Returns the HTML for this form submit button
+=cut
+sub html
+{
+my ($self) = @_;
+return &ui_submit($self->get_value(), $self->get_name(),
+ $self->get_disabled());
+}
+
+sub get_value
+{
+my ($self) = @_;
+return $self->{'value'};
+}
+
+1;
+
--- /dev/null
+package Webmin::Table;
+use Webmin::JavascriptButton;
+use WebminCore;
+
+=head2 new Webmin::Table(&headings, [width], [name], [heading])
+Create a multi-column table, with support for sorting, paging and so on
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::Table::new) &&
+ caller() !~ /Webmin::Theme::Table/) {
+ return new Webmin::Theme::Table(@_[1..$#_]);
+ }
+my ($self, $headings, $width, $name, $heading) = @_;
+$self = { 'sorter' => [ map { \&default_sorter } @$headings ] };
+bless($self);
+$self->set_headings($headings);
+$self->set_name($name) if (defined($name));
+$self->set_width($width) if (defined($width));
+$self->set_heading($heading) if (defined($heading));
+$self->set_all_sortable(1);
+$self->set_paging(1);
+return $self;
+}
+
+=head2 add_row(&fields)
+Adds a row to the table. Each element in the row can be either an input of some
+kind, or a piece of text.
+=cut
+sub add_row
+{
+my ($self, $fields) = @_;
+push(@{$self->{'rows'}}, $fields);
+}
+
+=head2 html()
+Returns the HTML for this table. The actual ordering may depend upon sort headers
+clicked by the user. The rows to display may be limited by the page size.
+=cut
+sub html
+{
+my ($self) = @_;
+my @srows = @{$self->{'rows'}};
+my $thisurl = $self->{'form'}->{'page'}->get_myurl();
+my $name = $self->get_name();
+my $rv;
+
+# Add the heading
+if ($self->get_heading()) {
+ $rv .= &ui_subheading($self->get_heading())."\n";
+ }
+
+my $sm = $self->get_searchmax();
+if (defined($sm) && @srows > $sm) {
+ # Too many rows to show .. add a search form. This will need to close
+ # the parent form, and then re-open it after the search form, as nested
+ # forms aren't allowed!
+ if ($self->get_searchmsg()) {
+ $rv .= $self->get_searchmsg()."<br>\n";
+ }
+
+ my $form = new Webmin::Form($thisurl, "get");
+ $form->set_input($self->{'form'}->{'in'});
+ my $section = new Webmin::Section(undef, 2);
+ $form->add_section($section);
+
+ my $col = new Webmin::Select("ui_searchcol_".$name, undef);
+ my $i = 0;
+ foreach my $h (@{$self->get_headings()}) {
+ if ($self->{'sortable'}->[$i]) {
+ $col->add_option($i, $h);
+ }
+ $i++;
+ }
+ $section->add_input($text{'ui_searchcol'}, $col);
+
+ my $for = new Webmin::Textbox("ui_searchfor_".$name, undef, 30);
+ $section->add_input($text{'ui_searchfor'}, $for);
+
+ $rv .= $section->html();
+ my $url = $self->make_url(undef, undef, undef, undef, 1);
+ my $jsb = new Webmin::JavascriptButton($text{'ui_searchok'},
+ "window.location = '$url'+'&'+'ui_searchfor_${name}'+'='+escape(form.ui_searchfor_${name}.value)+'&'+'ui_searchcol_${name}'+'='+escape(form.ui_searchcol_${name}.selectedIndex)");
+ $rv .= $jsb->html();
+ $rv .= "<br>\n";
+
+ # Limit records to current search
+ if (defined($col->get_value())) {
+ my $sf = $for->get_value();
+ @srows = grep { $_->[$col->get_value()] =~ /\Q$sf\E/i } @srows;
+ }
+ else {
+ @srows = ( );
+ }
+ }
+
+# Prepare the selector
+my $selc = $self->{'selectcolumn'};
+my $seli = $self->{'selectinput'};
+my %selmap;
+if (defined($selc)) {
+ my $i = 0;
+ foreach my $r (@srows) {
+ $selmap{$r,$selc} = $seli->one_html($i);
+ $i++;
+ }
+ }
+
+# Sort the rows
+my ($sortcol, $sortdir) = $self->get_sortcolumn();
+if (defined($sortcol)) {
+ my $func = $self->{'sorter'}->[$sortcol];
+ @srows = sort { my $so = &$func($a->[$sortcol], $b->[$sortcol], $sortcol);
+ $sortdir ? -$so : $so } @srows;
+ }
+
+# Build the td attributes
+my @tds = map { "valign=top" } @{$self->{'headings'}};
+if ($self->{'widths'}) {
+ my $i = 0;
+ foreach my $w (@{$self->{'widths'}}) {
+ $tds[$i++] .= " width=$w";
+ }
+ }
+if ($self->{'aligns'}) {
+ my $i = 0;
+ foreach my $a (@{$self->{'aligns'}}) {
+ $tds[$i++] .= " align=$a";
+ }
+ }
+
+# Find the page we want
+my $page = $self->get_pagepos();
+my ($start, $end, $origsize);
+if ($self->get_paging() && $self->get_pagesize()) {
+ # Restrict view to rows within some page
+ $start = $self->get_pagesize()*$page;
+ $end = $self->get_pagesize()*($page+1) - 1;
+ if ($start >= @srows) {
+ # Gone off end!
+ $start = 0;
+ $end = $self->get_pagesize()-1;
+ }
+ if ($end >= @srows) {
+ # End is too far
+ $end = @srows-1;
+ }
+ $origsize = scalar(@srows);
+ @srows = @srows[$start..$end];
+ }
+
+# Generate the headings, with sorters
+$thisurl .= $thisurl =~ /\?/ ? "&" : "?";
+my @sheadings;
+my $i = 0;
+foreach my $h (@{$self->get_headings()}) {
+ if ($self->{'sortable'}->[$i]) {
+ # Column can be sorted!
+ my $hh = "<table cellpadding=0 cellspacing=0 width=100%><tr>";
+ $hh .= "<td><b>$h</b></td> <td align=right>";
+ if (!defined($sortcol) || $sortcol != $i) {
+ # Not sorting on this column .. show grey button
+ my $url = $self->make_url($i, 0, undef, undef);
+ $hh .= "<a href='$url'>".
+ "<img src=/images/nosort.gif border=0></a>";
+ }
+ else {
+ # Sorting .. show button to switch mode
+ my $notsort = !$sortdir;
+ my $url = $self->make_url($i, $sortdir ? 0 : 1, undef, undef);
+ $hh .= "<a href='$url'>".
+ "<img src=/images/sort.gif border=0></a>";
+ }
+ $hh .= "</td></tr></table>";
+ push(@sheadings, $hh);
+ }
+ else {
+ push(@sheadings, $h);
+ }
+ $i++;
+ }
+
+# Get any errors for inputs
+my @errs = map { $_->get_errors() } $self->list_inputs();
+if (@errs) {
+ foreach my $e (@errs) {
+ $rv .= "<font color=#ff0000>$e</font><br>\n";
+ }
+ }
+
+# Build links for top and bottom
+my $links;
+if (ref($seli) =~ /Checkboxes/) {
+ # Add select all/none links
+ my $formno = $self->{'form'}->get_formno();
+ $links .= &select_all_link($seli->get_name(), $formno,
+ $text{'ui_selall'})."\n";
+ $links .= &select_invert_link($seli->get_name(), $formno,
+ $text{'ui_selinv'})."\n";
+ $links .= " \n";
+ }
+foreach my $l (@{$self->{'links'}}) {
+ $links .= "<a href='$l->[0]'>$l->[1]</a>\n";
+ }
+$links .= "<br>" if ($links);
+
+# Build list of inputs for bottom
+my $inputs;
+foreach my $i (@{$self->{'inputs'}}) {
+ $inputs .= $i->html()."\n";
+ }
+$inputs .= "<br>" if ($inputs);
+
+# Create the pager
+if ($self->get_paging() && $origsize) {
+ my $lastpage = int(($origsize-1)/$self->get_pagesize());
+ $rv .= "<center>";
+ if ($page != 0) {
+ # Add start and left arrows
+ my $surl = $self->make_url(undef, undef, undef, 0);
+ $rv .= "<a href='$surl'><img src=/images/first.gif border=0 align=middle></a>\n";
+ my $lurl = $self->make_url(undef, undef, undef, $page-1);
+ $rv .= "<a href='$lurl'><img src=/images/left.gif border=0 align=middle></a>\n";
+ }
+ else {
+ # Start and left are disabled
+ $rv .= "<img src=/images/first-grey.gif border=0 align=middle>\n";
+ $rv .= "<img src=/images/left-grey.gif border=0 align=middle>\n";
+ }
+ $rv .= &text('ui_paging', $start+1, $end+1, $origsize);
+ if ($end < $origsize-1) {
+ # Add right and end arrows
+ my $rurl = $self->make_url(undef, undef, undef, $page+1);
+ $rv .= "<a href='$rurl'><img src=/images/right.gif border=0 align=middle></a>\n";
+ my $eurl = $self->make_url(undef, undef, undef, $lastpage);
+ $rv .= "<a href='$eurl'><img src=/images/last.gif border=0 align=middle></a>\n";
+ }
+ else {
+ # Right and end are disabled
+ $rv .= "<img src=/images/right-grey.gif border=0 align=middle>\n";
+ $rv .= "<img src=/images/last-grey.gif border=0 align=middle>\n";
+ }
+ $rv .= "</center>\n";
+ }
+
+# Create actual table
+if (@srows) {
+ $rv .= $links;
+ $rv .= &ui_columns_start(\@sheadings, $self->{'width'}, 0, \@tds);
+ foreach my $r (@srows) {
+ my @row;
+ for(my $i=0; $i<@$r || $i<@sheadings; $i++) {
+ if (ref($r->[$i]) eq "ARRAY") {
+ my $j = $r->[$i]->[0] &&
+ $r->[$i]->[0]->isa("Webmin::TableAction")
+ ? " | " : " ";
+ $row[$i] = $selmap{$r,$i}.
+ join($j, map { ref($_) ? $_->html() : $_ }
+ @{$r->[$i]});
+ }
+ elsif (ref($r->[$i])) {
+ $row[$i] = $selmap{$r,$i}.$r->[$i]->html();
+ }
+ else {
+ $row[$i] = $selmap{$r,$i}.$r->[$i];
+ }
+ }
+ $rv .= &ui_columns_row(\@row, \@tds);
+ }
+ $rv .= &ui_columns_end();
+ }
+elsif ($self->{'emptymsg'}) {
+ $rv .= $self->{'emptymsg'}."<p>\n";
+ }
+$rv .= $links;
+$rv .= $inputs;
+return $rv;
+}
+
+=head2 set_form(form)
+Called by the Webmin::Form object when this table is added to it
+=cut
+sub set_form
+{
+my ($self, $form) = @_;
+$self->{'form'} = $form;
+foreach my $i ($self->list_inputs()) {
+ $i->set_form($form);
+ }
+}
+
+=head2 set_sorter(function, [column])
+Sets a function used for sorting fields. Will be called with two field values to
+compare, and a column number.
+=cut
+sub set_sorter
+{
+my ($self, $func, $col) = @_;
+if (defined($col)) {
+ $self->{'sorter'}->[$col] = $func;
+ }
+else {
+ $self->{'sorter'} = [ map { $func } @{$self->{'headings'}} ];
+ }
+}
+
+=head2 default_sorter(value1, value2, col)
+=cut
+sub default_sorter
+{
+my ($value1, $value2, $col) = @_;
+if (ref($value1) && $value1->isa("Webmin::TableAction")) {
+ $value1 = $value1->get_value();
+ $value2 = $value2->get_value();
+ }
+return lc($value1) cmp lc($value2);
+}
+
+=head2 numeric_sorter(value1, value2, col)
+=cut
+sub numeric_sorter
+{
+my ($value1, $value2, $col) = @_;
+return $value1 <=> $value2;
+}
+
+=head2 set_sortable(column, sortable?)
+Tells the table if some column should allow sorting or not. By default, all are.
+=cut
+sub set_sortable
+{
+my ($self, $col, $sortable) = @_;
+$self->{'sortable'}->[$col] = $sortable;
+}
+
+=head2 set_all_sortable(sortable?)
+Enabled or disables sorting for all columns
+=cut
+sub set_all_sortable
+{
+my ($self, $sortable) = @_;
+$self->{'sortable'} = [ map { $sortable } @{$self->{'headings'}} ];
+}
+
+=head2 get_sortcolumn()
+Returns the column to sort on and the order (1 for descending), or undef for none
+=cut
+sub get_sortcolumn
+{
+my ($self) = @_;
+my $in = $self->{'form'} ? $self->{'form'}->{'in'} : undef;
+my $name = $self->get_name();
+if ($in && defined($in->{"ui_sortcolumn_".$name})) {
+ return ( $in->{"ui_sortcolumn_".$name},
+ $in->{"ui_sortdir_".$name} );
+ }
+else {
+ return ( $self->{'sortcolumn'}, $self->{'sortdir'} );
+ }
+}
+
+=head2 set_sortcolumn(num, descending?)
+Sets the default column on which sorting will be done, unless overridden by
+the user.
+=cut
+sub set_sortcolumn
+{
+my ($self, $col, $desc) = @_;
+$self->{'sortcolumn'} = $col;
+$self->{'sortdir'} = $desc;
+}
+
+=head2 get_paging()
+Returns 1 if page-by-page display should be used
+=cut
+sub get_paging
+{
+my ($self) = @_;
+my $in = $self->{'form'} ? $self->{'form'}->{'in'} : undef;
+my $name = $self->get_name();
+if ($in && defined($in->{"ui_paging_".$name})) {
+ return ( $in->{"ui_paging_".$name} );
+ }
+else {
+ return ( $self->{'paging'} );
+ }
+}
+
+=head2 set_paging(paging?)
+Turns page-by-page display of the table on or off
+=cut
+sub set_paging
+{
+my ($self, $paging) = @_;
+$self->{'paging'} = $paging;
+}
+
+sub set_name
+{
+my ($self, $name) = @_;
+$self->{'name'} = $name;
+}
+
+=head2 get_name()
+Returns the name for indentifying this table in HTML
+=cut
+sub get_name
+{
+my ($self) = @_;
+if (defined($self->{'name'})) {
+ return $self->{'name'};
+ }
+elsif ($self->{'form'}) {
+ my $secs = $self->{'form'}->{'sections'};
+ for(my $i=0; $i<@$secs; $i++) {
+ return "table".$i if ($secs->[$i] eq $self);
+ }
+ }
+return "table";
+}
+
+sub set_headings
+{
+my ($self, $headings) = @_;
+$self->{'headings'} = $headings;
+}
+
+sub get_headings
+{
+my ($self) = @_;
+return $self->{'headings'};
+}
+
+=head2 set_selector(column, input)
+Takes a Webmin::Checkboxes or Webmin::Radios object, and uses it to add checkboxes
+in the specified column.
+=cut
+sub set_selector
+{
+my ($self, $col, $input) = @_;
+$self->{'selectcolumn'} = $col;
+$self->{'selectinput'} = $input;
+$input->set_form($form);
+}
+
+=head2 get_selector()
+Returns the UI element used for selecting rows
+=cut
+sub get_selector
+{
+my ($self) = @_;
+return wantarray ? ( $self->{'selectinput'}, $self->{'selectcolumn'} )
+ : $self->{'selectinput'};
+}
+
+=head2 set_widths(&widths)
+Given an array reference of widths (like 50 or 20%), uses them for the columns
+in the table.
+=cut
+sub set_widths
+{
+my ($self, $widths) = @_;
+$self->{'widths'} = $widths;
+}
+
+=head2 set_width([number|number%])
+Sets the width of this entire table. Can be called with 100%, 500 or undef to use
+the minimum possible width.
+=cut
+sub set_width
+{
+my ($self, $width) = @_;
+$self->{'width'} = $width;
+}
+
+=head2 set_aligns(&aligns)
+Given an array reference of horizontal alignments (like left or right), uses them
+for the columns in the table.
+=cut
+sub set_aligns
+{
+my ($self, $aligns) = @_;
+$self->{'aligns'} = $aligns;
+}
+
+=head2 validate()
+Validates all inputs, and returns a list of error messages
+=cut
+sub validate
+{
+my ($self) = @_;
+my $seli = $self->{'selectinput'};
+my @errs;
+if ($seli) {
+ push(@errs, map { [ $seli->get_name(), $_ ] } $seli->validate());
+ }
+foreach my $i ($self->list_inputs()) {
+ foreach my $e ($i->validate()) {
+ push(@errs, [ $i->get_name(), $e ]);
+ }
+ }
+return @errs;
+}
+
+=head2 get_value(input-name)
+Returns the value of the input with the given name.
+=cut
+sub get_value
+{
+my ($self, $name) = @_;
+if ($self->{'selectinput'} && $self->{'selectinput'}->get_name() eq $name) {
+ return $self->{'selectinput'}->get_value();
+ }
+foreach my $i ($self->list_inputs()) {
+ if ($i->get_name() eq $name) {
+ return $i->get_value();
+ }
+ }
+return undef;
+}
+
+=head2 list_inputs()
+Returns all inputs in all form sections
+=cut
+sub list_inputs
+{
+my ($self) = @_;
+my @rv = @{$self->{'inputs'}};
+push(@rv, $self->{'selectinput'}) if ($self->{'selectinput'});
+return @rv;
+}
+
+=head2 set_searchmax(num, [message])
+Sets the maximum number of table rows to display before showing a search form
+=cut
+sub set_searchmax
+{
+my ($self, $searchmax, $searchmsg) = @_;
+$self->{'searchmax'} = $searchmax;
+$self->{'searchmsg'} = $searchmsg;
+}
+
+sub get_searchmax
+{
+my ($self) = @_;
+return $self->{'searchmax'};
+}
+
+sub get_searchmsg
+{
+my ($self) = @_;
+return $self->{'searchmsg'};
+}
+
+=head2 add_link(link, message)
+Adds a link to the table, for example for adding a new entry
+=cut
+sub add_link
+{
+my ($self, $link, $msg) = @_;
+push(@{$self->{'links'}}, [ $link, $msg ]);
+}
+
+=head2 add_input(input)
+Adds some input to be displayed at the bottom of the table
+=cut
+sub add_input
+{
+my ($self, $input) = @_;
+push(@{$self->{'inputs'}}, $input);
+$input->set_form($self->{'form'});
+}
+
+=head2 set_emptymsg(message)
+Sets the message to display when the table is empty
+=cut
+sub set_emptymsg
+{
+my ($self, $emptymsg) = @_;
+$self->{'emptymsg'} = $emptymsg;
+}
+
+=head2 set_heading(text)
+Sets the heading text to appear above the table
+=cut
+sub set_heading
+{
+my ($self, $heading) = @_;
+$self->{'heading'} = $heading;
+}
+
+sub get_heading
+{
+my ($self) = @_;
+return $self->{'heading'};
+}
+
+=head2 set_pagesize(pagesize)
+Sets the size of a page. Set to 0 to turn off completely.
+=cut
+sub set_pagesize
+{
+my ($self, $pagesize) = @_;
+$self->{'pagesize'} = $pagesize;
+}
+
+=head2 get_pagesize()
+Returns the size of a page, or 0 if paging is turned off totally
+=cut
+sub get_pagesize
+{
+my ($self) = @_;
+return $self->{'pagesize'};
+}
+
+sub get_pagepos
+{
+my ($self) = @_;
+my $in = $self->{'form'} ? $self->{'form'}->{'in'} : undef;
+my $name = $self->get_name();
+if ($in && defined($in->{"ui_pagepos_".$name})) {
+ return ( $in->{"ui_pagepos_".$name} );
+ }
+else {
+ return ( $self->{'pagepos'} );
+ }
+}
+
+=head2 make_url(sortcol, sortdir, paging, page, [no-searchargs], [no-pagearg])
+Returns a link to this table's page, with the defaults for the various state
+fields overriden by the parameters (where defined)
+=cut
+sub make_url
+{
+my ($self, $newsortcol, $newsortdir, $newpaging, $newpagepos,
+ $nosearch, $nopage) = @_;
+my ($sortcol, $sortdir) = $self->get_sortcolumn();
+$sortcol = $newsortcol if (defined($newsortcol));
+$sortdir = $newsortdir if (defined($newsortdir));
+my $paging = $self->get_paging();
+$paging = $newpaging if (defined($newpaging));
+my $pagepos = $self->get_pagepos();
+$pagepos = $newpagepos if (defined($newpagepos));
+
+my $thisurl = $self->{'form'}->{'page'}->get_myurl();
+my $name = $self->get_name();
+$thisurl .= $thisurl =~ /\?/ ? "&" : "?";
+my $in = $self->{'form'}->{'in'};
+return "${thisurl}ui_sortcolumn_${name}=$sortcol".
+ "&ui_sortdir_${name}=$sortdir".
+ "&ui_paging_${name}=$paging".
+ ($nopage ? "" : "&ui_pagepos_${name}=$pagepos").
+ ($nosearch ? "" : "&ui_searchfor_${name}=".
+ &urlize($in->{"ui_searchfor_${name}"}).
+ "&ui_searchcol_${name}=".
+ &urlize($in->{"ui_searchcol_${name}"}));
+}
+
+1;
+
--- /dev/null
+package Webmin::TableAction;
+use WebminCore;
+
+=head2 new Webmin::TableAction(cgi, label, &args, disabled)
+An object of this class can be added to a table or properties object to create
+a link or action button of some kind.
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::TableAction::new) &&
+ caller() !~ /Webmin::Theme::TableAction/) {
+ return new Webmin::Theme::TableAction(@_[1..$#_]);
+ }
+my ($self, $cgi, $value, $args, $disabled) = @_;
+$self = { };
+bless($self);
+$self->set_value($value);
+$self->set_cgi($cgi);
+$self->set_args($args) if (defined($args));
+$self->set_disabled($disabled) if (defined($disabled));
+return $self;
+}
+
+sub html
+{
+my ($self) = @_;
+my $rv;
+if ($self->get_disabled()) {
+ $rv .= "<u><i>".$self->get_value()."</i></u>";
+ }
+else {
+ my $link = $self->get_cgi();
+ my $i = 0;
+ foreach my $a (@{$self->get_args()}) {
+ $link .= ($i++ ? "&" : "?");
+ $link .= &urlize($a->[0])."=".&urlize($a->[1]);
+ }
+ $rv .= "<a href='$link'>".$self->get_value()."</a>";
+ }
+return $rv;
+}
+
+sub set_value
+{
+my ($self, $value) = @_;
+$self->{'value'} = $value;
+}
+
+sub get_value
+{
+my ($self) = @_;
+return $self->{'value'};
+}
+
+sub set_cgi
+{
+my ($self, $cgi) = @_;
+$self->{'cgi'} = $cgi;
+}
+
+sub get_cgi
+{
+my ($self) = @_;
+return $self->{'cgi'};
+}
+
+sub set_args
+{
+my ($self, $args) = @_;
+$self->{'args'} = $args;
+}
+
+sub get_args
+{
+my ($self) = @_;
+return $self->{'args'};
+}
+
+sub set_disabled
+{
+my ($self, $disabled) = @_;
+$self->{'disabled'} = $disabled;
+}
+
+sub get_disabled
+{
+my ($self) = @_;
+return $self->{'disabled'};
+}
+
+1;
+
--- /dev/null
+package Webmin::Tabs;
+use WebminCore;
+
+=head2 new Webmin::Tabs([&tabs])
+Displayed at the top of a page, to allow selection of various pages
+=cut
+sub new
+{
+my ($self, $tabs) = @_;
+if (defined(&Webmin::Theme::Tabs::new)) {
+ return new Webmin::Theme::Tabs(@_[1..$#_]);
+ }
+$self = { 'tabs' => [ ],
+ 'tab' => 0 };
+bless($self);
+$self->set_tabs($tabs) if (defined($tabs));
+return $self;
+}
+
+=head2 add_tab(name, link)
+=cut
+sub add_tab
+{
+my ($self, $name, $link) = @_;
+push(@{$self->{'tabs'}}, [ $name, $link ]);
+}
+
+=head2 html()
+Returns the HTML for the top of the page
+=cut
+sub top_html
+{
+my ($self) = @_;
+my $rv;
+$rv .= "<table border=0 cellpadding=0 cellspacing=0 width=100% height=20><tr>";
+$rv .= "<td valign=bottom>";
+$rv .= "<table border=0 cellpadding=0 cellspacing=0 height=20><tr>";
+my $i = 0;
+my ($high, $low) = ("#cccccc", "#9999ff");
+my ($lowlc, $lowrc) = ( "/images/lc1.gif", "/images/rc1.gif" );
+my ($highlc, $highrc) = ( "/images/lc2.gif", "/images/rc2.gif" );
+foreach my $t (@{$self->get_tabs()}) {
+ if ($i == $self->get_tab()) {
+ # This is the selected tab
+ $rv .= "<td valign=top bgcolor=$high>".
+ "<img src=$highlc alt=\"\"></td>";
+ if ($self->get_link()) {
+ # Link
+ $rv .= "<td bgcolor=$high> ".
+ "<a href=$t->[1]><b>$t->[0]</b></a> </td>";
+ }
+ else {
+ # Don't link
+ $rv .= "<td bgcolor=$high> <b>$t->[0]</b> </td>";
+ }
+ $rv .= "<td valign=top bgcolor=$high>".
+ "<img src=$highrc alt=\"\"></td>\n";
+ }
+ else {
+ # Not selected
+ $rv .= "<td valign=top bgcolor=$low>".
+ "<img src=$lowlc alt=\"\"></td>";
+ $rv .= "<td bgcolor=$low> ".
+ "<a href=$t->[1]><b>$t->[0]</b></a> </td>";
+ $rv .= "<td valign=top bgcolor=$low>".
+ "<img src=$lowrc alt=\"\"></td>\n";
+ }
+ $i++;
+ if ($self->{'wrap'} && $i%$self->{'wrap'} == 0) {
+ # New row
+ $rv .= "</tr><tr>";
+ }
+ }
+$rv .= "</tr></table></td>\n";
+$rv .= "</tr></table>\n";
+$rv .= "<table border=1 cellpadding=10 cellspacing=0 width=100%><tr><td>\n";
+return $rv;
+}
+
+=head2 bottom_html()
+Returns the HTML for the bottom of the page
+=cut
+sub bottom_html
+{
+my ($self) = @_;
+my $rv = "</td></tr></table>\n";
+return $rv;
+}
+
+=head2 set_tab(number|link)
+Sets the tab that is currently highlighted
+=cut
+sub set_tab
+{
+my ($self, $tab) = @_;
+if ($tab =~ /^\d+$/) {
+ $self->{'tab'} = $tab;
+ }
+else {
+ for(my $i=0; $i<@{$self->{'tabs'}}; $i++) {
+ if ($self->{'tabs'}->[$i]->[1] eq $tab) {
+ $self->{'tab'} = $i;
+ }
+ }
+ }
+}
+
+sub get_tab
+{
+my ($self) = @_;
+return $self->{'tab'};
+}
+
+=head2 set_link(link)
+If called with a non-zero parameter, even the highlighted tab will be a link
+=cut
+sub set_link
+{
+my ($self, $link) = @_;
+$self->{'link'} = $link;
+}
+
+sub get_link
+{
+my ($self) = @_;
+return $self->{'link'};
+}
+
+sub set_tabs
+{
+my ($self, $tabs) = @_;
+$self->{'tabs'} = $tabs;
+}
+
+sub get_tabs
+{
+my ($self) = @_;
+return $self->{'tabs'};
+}
+
+1;
+
--- /dev/null
+package Webmin::Textarea;
+use Webmin::Input;
+use WebminCore;
+@ISA = ( "Webmin::Input" );
+
+=head2 new Webmin::Textarea(name, value, rows, cols, [wrap], [disabled])
+Create a new text box, with the given size
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::Textarea::new)) {
+ return new Webmin::Theme::Textarea(@_[1..$#_]);
+ }
+my ($self, $name, $value, $rows, $cols, $wrap, $disabled) = @_;
+$self = { };
+bless($self);
+$self->set_name($name);
+$self->set_value($value);
+$self->set_rows($rows);
+$self->set_cols($cols);
+$self->set_disabled($disabled);
+return $self;
+}
+
+=head2 html()
+Returns the HTML for this text area
+=cut
+sub html
+{
+my ($self) = @_;
+return &ui_textarea($self->get_name(), $self->get_value(),
+ $self->get_rows(), $self->get_cols(),
+ $self->get_wrap(), $self->get_disabled());
+}
+
+sub set_rows
+{
+my ($self, $rows) = @_;
+$self->{'rows'} = $rows;
+}
+
+sub get_rows
+{
+my ($self) = @_;
+return $self->{'rows'};
+}
+
+sub set_cols
+{
+my ($self, $cols) = @_;
+$self->{'cols'} = $cols;
+}
+
+sub get_cols
+{
+my ($self) = @_;
+return $self->{'cols'};
+}
+
+sub set_wrap
+{
+my ($self, $wrap) = @_;
+$self->{'wrap'} = $wrap;
+}
+
+sub get_wrap
+{
+my ($self) = @_;
+return $self->{'wrap'};
+}
+
+sub set_validation_func
+{
+my ($self, $func) = @_;
+$self->{'validation_func'} = $func;
+}
+
+=head2 set_validation_regexp(regexp, message)
+=cut
+sub set_validation_regexp
+{
+my ($self, $regexp, $message) = @_;
+$self->{'validation_regexp'} = $regexp;
+$self->{'validation_message'} = $message;
+}
+
+=head2 validate()
+Returns a list of error messages for this field
+=cut
+sub validate
+{
+my ($self) = @_;
+my $value = $self->get_value();
+if ($self->{'mandatory'} && $value eq '') {
+ return ( $self->{'mandmesg'} || $text{'ui_mandatory'} );
+ }
+if ($self->{'validation_func'}) {
+ my $err = &{$self->{'validation_func'}}($value, $self->{'name'},
+ $self->{'form'});
+ return ( $err ) if ($err);
+ }
+if ($self->{'validation_regexp'}) {
+ if ($value !~ /$self->{'validation_regexp'}/) {
+ return ( $self->{'validation_message'} );
+ }
+ }
+return ( );
+}
+
+=head2 get_value()
+Returns the value, without any \r characters
+=cut
+sub get_value
+{
+my ($self) = @_;
+my $rv = Webmin::Input::get_value($self);
+$rv =~ s/\r//g;
+return $rv;
+}
+
+1;
+
--- /dev/null
+package Webmin::Textbox;
+use Webmin::Input;
+use WebminCore;
+@ISA = ( "Webmin::Input" );
+
+=head2 new Webmin::Textbox(name, value, [size], [disabled])
+Create a new text input field
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::Textbox::new)) {
+ return new Webmin::Theme::Textbox(@_[1..$#_]);
+ }
+my ($self, $name, $value, $size, $disabled) = @_;
+$self = { 'size' => 30 };
+bless($self);
+$self->{'name'} = $name;
+$self->{'value'} = $value;
+$self->{'size'} = $size if ($size);
+$self->set_disabled($disabled) if (defined($disabled));
+return $self;
+}
+
+=head2 html()
+Returns the HTML for this text input
+=cut
+sub html
+{
+my ($self) = @_;
+return &ui_textbox($self->get_name(), $self->get_value(),
+ $self->{'size'},
+ $self->{'$disabled'});
+}
+
+sub set_size
+{
+my ($self, $size) = @_;
+$self->{'size'} = $size;
+}
+
+sub set_validation_func
+{
+my ($self, $func) = @_;
+$self->{'validation_func'} = $func;
+}
+
+=head2 set_validation_regexp(regexp, message)
+=cut
+sub set_validation_regexp
+{
+my ($self, $regexp, $message) = @_;
+$self->{'validation_regexp'} = $regexp;
+$self->{'validation_message'} = $message;
+}
+
+=head2 validate()
+Returns a list of error messages for this field
+=cut
+sub validate
+{
+my ($self) = @_;
+my $value = $self->get_value();
+if ($self->{'mandatory'} && $value eq '') {
+ return ( $self->{'mandmesg'} || $text{'ui_mandatory'} );
+ }
+if ($self->{'validation_func'}) {
+ my $err = &{$self->{'validation_func'}}($value, $self->{'name'},
+ $self->{'form'});
+ return ( $err ) if ($err);
+ }
+if ($self->{'validation_regexp'}) {
+ if ($value !~ /$self->{'validation_regexp'}/) {
+ return ( $self->{'validation_message'} );
+ }
+ }
+return ( );
+}
+
+1;
+
--- /dev/null
+package Webmin::Time;
+use Webmin::Input;
+use Time::Local;
+use WebminCore;
+@ISA = ( "Webmin::Input" );
+
+=head2 new Webmin::Time(name, time, [disabled])
+Create a new field for selecting a time
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::Time::new)) {
+ return new Webmin::Theme::Time(@_[1..$#_]);
+ }
+my ($self, $name, $value, $disabled) = @_;
+bless($self = { });
+$self->set_name($name);
+$self->set_value($value);
+$self->set_disabled($disabled) if (defined($disabled));
+return $self;
+}
+
+=head2 html()
+Returns the HTML for the time chooser
+=cut
+sub html
+{
+my ($self) = @_;
+my $rv;
+my $val = $self->get_value();
+my $hour = ($val/3600) % 24;
+my $min = ($val/60) % 60;
+my $sec = ($val/1) % 60;
+my $name = $self->get_name();
+$rv .= &ui_textbox("hour_".$name, pad2($hour), 2,$self->get_disabled()).":".
+ &ui_textbox("min_".$name, pad2($min), 2, $self->get_disabled()).":".
+ &ui_textbox("sec_".$name, pad2($sec), 2, $self->get_disabled());
+return $rv;
+}
+
+sub pad2
+{
+return $_[0] < 10 ? "0".$_[0] : $_[0];
+}
+
+sub set_value
+{
+my ($self, $value) = @_;
+$self->{'value'} = timegm(localtime($value));
+}
+
+=head2 get_value()
+Returns the date as a Unix time number (for 1st jan 1970)
+=cut
+sub get_value
+{
+my ($self) = @_;
+my $in = $self->{'form'} ? $self->{'form'}->{'in'} : undef;
+if ($in && defined($in->{"hour_".$self->{'name'}})) {
+ my $rv = $self->to_time($in);
+ return defined($rv) ? $rv : $self->{'value'};
+ }
+elsif ($in && defined($in->{"ui_value_".$self->{'name'}})) {
+ return $in->{"ui_value_".$self->{'name'}};
+ }
+else {
+ return $self->{'value'};
+ }
+}
+
+sub to_time
+{
+my ($self, $in) = @_;
+my $hour = $in->{"hour_".$self->{'name'}};
+return undef if ($hour !~ /^\d+$/ || $hour < 0 || $hour > 23);
+my $min = $in->{"min_".$self->{'name'}};
+return undef if ($min !~ /^\d+$/ || $min < 0 || $min > 59);
+my $sec = $in->{"sec_".$self->{'name'}};
+return undef if ($sec !~ /^\d+$/ || $sec < 0 || $sec > 59);
+return $hour*60*60 + $min*60 + $sec;
+}
+
+sub set_validation_func
+{
+my ($self, $func) = @_;
+$self->{'validation_func'} = $func;
+}
+
+=head2 validate()
+Ensures that the date is valid
+=cut
+sub validate
+{
+my ($self) = @_;
+my $tm = $self->to_time($self->{'form'}->{'in'});
+if (!defined($tm)) {
+ return ( $text{'ui_etime'} );
+ }
+if ($self->{'validation_func'}) {
+ my $err = &{$self->{'validation_func'}}($self->get_value(),
+ $self->{'name'},
+ $self->{'form'});
+ return ( $err ) if ($err);
+ }
+return ( );
+}
+
+=head2 set_auto(auto?)
+If set to 1, the time will be automatically incremented by Javascript
+=cut
+sub set_auto
+{
+my ($self, $auto) = @_;
+$self->{'auto'} = $auto;
+if ($auto) {
+ # XXX incorrect!!
+ my $formno = $self->{'form'}->get_formno();
+ $self->{'form'}->add_onload("F=[0]; timeInit(F); setTimeout(\"timeUpdate(F)\", 5000)");
+ my $as = $autoscript;
+ $as =~ s/NAME/$self->{'name'}/g;
+ $self->{'form'}->add_script($as);
+ }
+}
+
+$autoscript = <<EOF;
+function timeInit(F) {
+ secs = new Array();
+ mins = new Array();
+ hours = new Array();
+ for(i=0; i<F.length; i++){
+ secs[i] = document.forms[F[i]].sec_NAME;
+ mins[i] = document.forms[F[i]].min_NAME;
+ hours[i] = document.forms[F[i]].hour_NAME;
+ }
+}
+function timeUpdate(F) {
+ for(i=0; i<F.length; i++){
+ s = parseInt(secs[i].value);
+ s = s ? s : 0;
+ s = s+5;
+ if( s>59 ){
+ s -= 60;
+ m = parseInt(mins[i].value);
+ m= m ? m : 0;
+ m+=1;
+ if( m>59 ){
+ m -= 60;
+ h = parseInt(hours[i].value);
+ h = h ? h : 0;
+ h+=1;
+ if( h>23 ){
+ h -= 24;
+ }
+ hours[i].value = packNum(h);
+ }
+ mins[i].value = packNum(m);
+ }
+ secs[i].value = packNum(s);
+ }
+ setTimeout('timeUpdate(F)', 5000);
+}
+function packNum(t) {
+ return (t < 10 ? '0'+t : t);
+}
+EOF
+
+1;
+
--- /dev/null
+package Webmin::TitleList;
+use WebminCore;
+
+=head2 new Webmin::TitleList(title, &links, [alt-text])
+Generates a title with a list of links under it
+=cut
+sub new
+{
+my ($self, $title, $links, $alt) = @_;
+if (defined(&Webmin::Theme::TitleList::new)) {
+ return new Webmin::Theme::TitleList(@_[1..$#_]);
+ }
+$self = { };
+bless($self);
+$self->set_title($title);
+$self->set_links($links);
+$self->set_alt($alt) if (defined($alt));
+return $self;
+}
+
+=head2 html()
+Returns the list
+=cut
+sub html
+{
+my ($self) = @_;
+my $rv;
+if (defined(&ui_subheading)) {
+ $rv .= &ui_subheading($self->get_title());
+ }
+else {
+ $rv .= "<h3>".$self->get_title()."</h3>\n";
+ }
+$rv .= "<hr>\n";
+foreach my $l (@{$self->get_links()}) {
+ if ($l->[1]) {
+ $rv .= "<a href='$l->[1]'>$l->[0]</a><br>\n";
+ }
+ else {
+ $rv .= $l->[0]."<br>\n";
+ }
+ }
+return $rv;
+}
+
+sub set_title
+{
+my ($self, $title) = @_;
+$self->{'title'} = $title;
+}
+
+sub get_title
+{
+my ($self) = @_;
+return $self->{'title'};
+}
+
+sub set_links
+{
+my ($self, $links) = @_;
+$self->{'links'} = $links;
+}
+
+sub get_links
+{
+my ($self) = @_;
+return $self->{'links'};
+}
+
+sub set_alt
+{
+my ($self, $alt) = @_;
+$self->{'alt'} = $alt;
+}
+
+sub get_alt
+{
+my ($self) = @_;
+return $self->{'alt'};
+}
+
+=head2 add_link(name, link)
+Adds a link to be displayed in the list
+=cut
+sub add_link
+{
+my ($self, $name, $link) = @_;
+push(@{$self->{'links'}}, [ $name, $link ]);
+}
+
+=head2 set_page(Webmin::Page)
+Called when this menu is added to a page
+=cut
+sub set_page
+{
+my ($self, $page) = @_;
+$self->{'page'} = $page;
+}
+
+1;
+
--- /dev/null
+package Webmin::Upload;
+use Webmin::Input;
+use WebminCore;
+@ISA = ( "Webmin::Input" );
+
+=head2 new Webmin::Upload(name, [size])
+Create a new file upload field
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::Upload::new)) {
+ return new Webmin::Theme::Upload(@_[1..$#_]);
+ }
+my ($self, $name, $size) = @_;
+$self = { 'size' => 30 };
+bless($self);
+$self->{'name'} = $name;
+$self->{'size'} = $size if ($size);
+return $self;
+}
+
+=head2 html()
+Returns the HTML for this text input
+=cut
+sub html
+{
+my ($self) = @_;
+return &ui_upload($self->get_name(), $self->{'size'},
+ $self->{'$disabled'});
+}
+
+sub set_size
+{
+my ($self, $size) = @_;
+$self->{'size'} = $size;
+}
+
+sub set_validation_func
+{
+my ($self, $func) = @_;
+$self->{'validation_func'} = $func;
+}
+
+=head2 set_validation_regexp(regexp, message)
+=cut
+sub set_validation_regexp
+{
+my ($self, $regexp, $message) = @_;
+$self->{'validation_regexp'} = $regexp;
+$self->{'validation_message'} = $message;
+}
+
+=head2 validate()
+Returns a list of error messages for this field
+=cut
+sub validate
+{
+my ($self) = @_;
+my $value = $self->get_value();
+if ($self->{'mandatory'} && $value eq '') {
+ return ( $self->{'mandmesg'} || $text{'ui_mandatory'} );
+ }
+if ($self->{'validation_func'}) {
+ my $err = &{$self->{'validation_func'}}($value, $self->{'name'},
+ $self->{'in'});
+ return ( $err ) if ($err);
+ }
+if ($self->{'validation_regexp'}) {
+ if ($value !~ /$self->{'validation_regexp'}/) {
+ return ( $self->{'validation_message'} );
+ }
+ }
+return ( );
+}
+
+1;
+
--- /dev/null
+package Webmin::User;
+use Webmin::Textbox;
+use WebminCore;
+@ISA = ( "Webmin::Textbox" );
+
+=head2 new Webmin::User(name, value, [multiple], [disabled])
+A text box for entering or selecting one or many Unix usernames
+=cut
+sub new
+{
+if (defined(&Webmin::Theme::User::new)) {
+ return new Webmin::Theme::User(@_[1..$#_]);
+ }
+my ($self, $name, $value, $multiple, $disabled) = @_;
+$self = new Webmin::Textbox($name, $value, $multiple ? 40 : 15, $disabled);
+bless($self);
+$self->set_multiple($multiple);
+return $self;
+}
+
+=head2 html()
+Returns the HTML for this user input
+=cut
+sub html
+{
+my ($self) = @_;
+my $rv = Webmin::Textbox::html($self);
+my $name = $self->get_name();
+my $multiple = $self->get_multiple();
+local $w = $multiple ? 500 : 300;
+$rv .= " <input type=button name=${name}_button onClick='ifield = form.$name; chooser = window.open(\"$gconfig{'webprefix'}/user_chooser.cgi?multi=$multiple&user=\"+escape(ifield.value), \"chooser\", \"toolbar=no,menubar=no,scrollbars=yes,width=$w,height=200\"); chooser.ifield = ifield; window.ifield = ifield' value=\"...\">\n";
+return $rv;
+}
+
+sub set_multiple
+{
+my ($self, $multiple) = @_;
+$self->{'multiple'} = $multiple;
+}
+
+sub get_multiple
+{
+my ($self) = @_;
+return $self->{'multiple'};
+}
+
+=head2 get_input_names()
+Returns the actual names of all HTML elements that make up this input
+=cut
+sub get_input_names
+{
+my ($self) = @_;
+return ( $self->{'name'}, $self->{'name'}."_button" );
+}
+
+1;
+