Handle hostnames with upper-case letters
[webmin.git] / custom / custom-lib.pl
1 # custom-lib.pl
2 # Functions for storing custom commands
3
4 BEGIN { push(@INC, ".."); };
5 use WebminCore;
6 &init_config();
7 %access = &get_module_acl();
8
9 # list_commands()
10 # Returns a list of all custom commands
11 sub list_commands
12 {
13 local (@rv, $f);
14 local $mcd = $module_info{'usermin'} ? $config{'webmin_config'}
15                                      : $module_config_directory;
16 opendir(DIR, $mcd);
17 while($f = readdir(DIR)) {
18         local %cmd;
19         if ($f =~ /^(\d+)\.cmd$/) {
20                 # Read custom-command file
21                 $cmd{'file'} = "$mcd/$f";
22                 $cmd{'id'} = $1;
23                 open(FILE, $cmd{'file'});
24                 chop($cmd{'cmd'} = <FILE>);
25                 chop($cmd{'desc'} = <FILE>);
26                 local @o = split(/\s+/, <FILE>);
27                 $cmd{'user'} = $o[0];
28                 $cmd{'raw'} = int($o[1]);
29                 $cmd{'su'} = int($o[2]);
30                 $cmd{'order'} = int($o[3]);
31                 $cmd{'noshow'} = int($o[4]);
32                 $cmd{'usermin'} = int($o[5]);
33                 $cmd{'timeout'} = int($o[6]);
34                 $cmd{'clear'} = int($o[7]);
35                 $cmd{'format'} = $o[8] eq '-' ? undef : $o[8];
36                 }
37         elsif ($f =~ /^(\d+)\.edit$/) {
38                 # Read file-editor file
39                 $cmd{'file'} = "$mcd/$f";
40                 $cmd{'id'} = $1;
41                 open(FILE, $cmd{'file'});
42                 chop($cmd{'edit'} = <FILE>);
43                 chop($cmd{'desc'} = <FILE>);
44                 chop($cmd{'user'} = <FILE>);
45                 chop($cmd{'group'} = <FILE>);
46                 chop($cmd{'perms'} = <FILE>);
47                 chop($cmd{'before'} = <FILE>);
48                 chop($cmd{'after'} = <FILE>);
49                 chop($cmd{'order'} = <FILE>);
50                 $cmd{'order'} = int($cmd{'order'});
51                 chop($cmd{'usermin'} = <FILE>);
52                 chop($cmd{'envs'} = <FILE>);
53                 chop($cmd{'beforeedit'} = <FILE>);
54                 }
55         elsif ($f =~ /^(\d+)\.sql$/) {
56                 # Read SQL file
57                 $cmd{'file'} = "$mcd/$f";
58                 $cmd{'id'} = $1;
59                 open(FILE, $cmd{'file'});
60                 chop($cmd{'desc'} = <FILE>);
61                 chop($cmd{'type'} = <FILE>);
62                 chop($cmd{'db'} = <FILE>);
63                 chop($cmd{'user'} = <FILE>);
64                 chop($cmd{'pass'} = <FILE>);
65                 chop($cmd{'host'} = <FILE>);
66                 chop($cmd{'sql'} = <FILE>);
67                 $cmd{'sql'} =~ s/\t/\n/g;
68                 chop($cmd{'order'} = <FILE>);
69                 }
70         if (%cmd) {
71                 # Read common stuff
72                 while(<FILE>) {
73                         s/\r|\n//g;
74                         local @a = split(/:/, $_, 5);
75                         local ($quote, $must) = split(/,/, $a[3]);
76                         push(@{$cmd{'args'}}, { 'name' => $a[0],
77                                                 'type' => $a[1],
78                                                 'opts' => $a[2],
79                                                 'quote' => int($quote),
80                                                 'must' => int($must),
81                                                 'desc' => $a[4] });
82                         }
83                 close(FILE);
84                 $cmd{'index'} = scalar(@rv);
85                 open(HTML, "$mcd/$cmd{'id'}.html");
86                 while(<HTML>) {
87                         $cmd{'html'} .= $_;
88                         }
89                 close(HTML);
90
91                 # Read cluster hosts file
92                 open(CLUSTER, "$mcd/$cmd{'id'}.hosts");
93                 while(<CLUSTER>) {
94                         s/\r|\n//g;
95                         push(@{$cmd{'hosts'}}, $_);
96                         }
97                 close(CLUSTER);
98
99                 push(@rv, \%cmd);
100                 }
101         }
102 closedir(DIR);
103 return @rv;
104 }
105
106 # sort_commands(&command, ...)
107 # Sorts a list of custom commands by the user-defined order
108 sub sort_commands
109 {
110 local @cust = @_;
111 if ($config{'sort'}) {
112         @cust = sort { lc($a->{$config{'sort'}}) cmp
113                        lc($b->{$config{'sort'}}) } @cust;
114         }
115 else {
116         @cust = sort { local $o = $b->{'order'} <=> $a->{'order'};
117                        $o ? $o : $a->{'id'} <=> $b->{'id'} } @cust;
118         }
119 return @cust;
120 }
121
122 # get_command(id)
123 # Returns the command with some ID
124 sub get_command
125 {
126 local ($id, $idx) = @_;
127 local @cmds = &list_commands();
128 local $cmd;
129 if ($id) {
130         ($cmd) = grep { $_->{'id'} eq $id } &list_commands();
131         }
132 else {
133         $cmd = $cmds[$idx];
134         }
135 return $cmd;
136 }
137
138 # save_command(&command)
139 sub save_command
140 {
141 local $c = $_[0];
142 if ($c->{'edit'}) {
143         # Save a file editor
144         &open_lock_tempfile(FILE, ">$module_config_directory/$c->{'id'}.edit");
145         &print_tempfile(FILE, $c->{'edit'},"\n");
146         &print_tempfile(FILE, $c->{'desc'},"\n");
147         &print_tempfile(FILE, $c->{'user'},"\n");
148         &print_tempfile(FILE, $c->{'group'},"\n");
149         &print_tempfile(FILE, $c->{'perms'},"\n");
150         &print_tempfile(FILE, $c->{'before'},"\n");
151         &print_tempfile(FILE, $c->{'after'},"\n");
152         &print_tempfile(FILE, $c->{'order'},"\n");
153         &print_tempfile(FILE, $c->{'usermin'},"\n");
154         &print_tempfile(FILE, $c->{'envs'},"\n");
155         &print_tempfile(FILE, $c->{'beforeedit'},"\n");
156         }
157 elsif ($c->{'sql'}) {
158         # Save an SQL command
159         &open_lock_tempfile(FILE, ">$module_config_directory/$c->{'id'}.sql");
160         &print_tempfile(FILE, $c->{'desc'},"\n");
161         &print_tempfile(FILE, $c->{'type'},"\n");
162         &print_tempfile(FILE, $c->{'db'},"\n");
163         &print_tempfile(FILE, $c->{'user'},"\n");
164         &print_tempfile(FILE, $c->{'pass'},"\n");
165         &print_tempfile(FILE, $c->{'host'},"\n");
166         local $sql = $c->{'sql'};
167         $sql =~ s/\n/\t/g;
168         &print_tempfile(FILE, $sql,"\n");
169         &print_tempfile(FILE, $c->{'order'},"\n");
170         }
171 else {
172         # Save a custom command
173         &open_lock_tempfile(FILE, ">$module_config_directory/$c->{'id'}.cmd");
174         &print_tempfile(FILE, $c->{'cmd'},"\n");
175         &print_tempfile(FILE, $c->{'desc'},"\n");
176         &print_tempfile(FILE,
177                    $c->{'user'}," ",int($c->{'raw'})," ",int($c->{'su'})," ",
178                    int($c->{'order'})," ",int($c->{'noshow'})," ",
179                    int($c->{'usermin'})," ",int($c->{'timeout'})," ",
180                    int($c->{'clear'})," ",($c->{'format'} || "-"),"\n");
181         }
182
183
184 # Save parameters
185 foreach $a (@{$c->{'args'}}) {
186         &print_tempfile(FILE, $a->{'name'},":",$a->{'type'},":",
187            $a->{'opts'},":",int($a->{'quote'}),",",int($a->{'must'}),":",
188            $a->{'desc'},"\n");
189         }
190 &close_tempfile(FILE);
191
192 # Save HTML description file
193 &lock_file("$module_config_directory/$c->{'id'}.html");
194 if ($cmd->{'html'}) {
195         &open_tempfile(HTML, ">$module_config_directory/$c->{'id'}.html");
196         &print_tempfile(HTML, $cmd->{'html'});
197         &close_tempfile(HTML);
198         }
199 else {
200         unlink("$module_config_directory/$c->{'id'}.html");
201         }
202 &unlock_file("$module_config_directory/$c->{'id'}.html");
203
204 # Save cluster hosts
205 &lock_file("$module_config_directory/$c->{'id'}.hosts");
206 if (@{$cmd->{'hosts'}}) {
207         &open_tempfile(CLUSTER, ">$module_config_directory/$c->{'id'}.hosts");
208         foreach my $h (@{$cmd->{'hosts'}}) {
209                 &print_tempfile(CLUSTER, "$h\n");
210                 }
211         &close_tempfile(CLUSTER);
212         }
213 else {
214         unlink("$module_config_directory/$c->{'id'}.hosts");
215         }
216 &unlock_file("$module_config_directory/$c->{'id'}.hosts");
217 }
218
219 # delete_command(&command)
220 sub delete_command
221 {
222 local $f = "$module_config_directory/$_[0]->{'id'}".
223            ($_[0]->{'edit'} ? ".edit" : $_[0]->{'sql'} ? ".sql" : ".cmd");
224 &lock_file($f);
225 unlink($f);
226 &unlock_file($f);
227
228 # Delete HTML file
229 local $hf = "$module_config_directory/$_[0]->{'id'}.html";
230 if (-r $hf) {
231         &lock_file($hf);
232         unlink($hf);
233         &unlock_file($hf);
234         }
235
236 # Delete cluster file
237 local $cf = "$module_config_directory/$_[0]->{'id'}.hosts";
238 if (-r $cf) {
239         &lock_file($cf);
240         unlink($cf);
241         &unlock_file($cf);
242         }
243 }
244
245 sub can_run_command
246 {
247 if ($module_info{'usermin'}) {
248         # Only modules marked as for Usermin are considered
249         return 0 if (!$_[0]->{'usermin'});
250
251         # Check detailed access control list (if any)
252         return 1 if (!$config{'access'});
253         local @uinfo = @remote_user_info;
254         @uinfo = getpwnam($remote_user) if (!@uinfo);
255         local $l;
256         foreach $l (split(/\t/, $config{'access'})) {
257                 if ($l =~ /^(\S+):\s*(.*)$/) {
258                         local ($user, $ids) = ($1, $2);
259                         local $applies;
260                         if ($user =~ /^\@(.*)$/) {
261                                 # Check if user is in group
262                                 local @ginfo = getgrnam($1);
263                                 $applies++
264                                    if (@ginfo && ($ginfo[2] == $uinfo[3] ||
265                                        &indexof($remote_user,
266                                                 split(/\s+/, $ginfo[3])) >= 0));
267                                 }
268                         elsif ($user eq $remote_user || $user eq "*") {
269                                 $applies++;
270                                 }
271                         if ($applies) {
272                                 # Rule is for this user - check list
273                                 local @ids = split(/\s+/, $ids);
274                                 local $d;
275                                 foreach $d (@ids) {
276                                         return 1 if ($d eq '*' ||
277                                                      $_[0]->{'id'} eq $d);
278                                         return 0 if ("!".$_[0]->{'id'} eq $d);
279                                         }
280                                 return 0;
281                                 }
282                         }
283                 }
284         return 0;
285         }
286 else {
287         # Just use Webmin user's list of databases
288         local $c;
289         local $found;
290         return 1 if ($access{'cmds'} eq '*');
291         local @cmds = split(/\s+/, $access{'cmds'});
292         foreach $c (@cmds) {
293                 $found++ if ($c eq $_[0]->{'id'});
294                 }
295         return $cmds[0] eq '!' ? !$found : $found;
296         }
297 }
298
299 # read_opts_file(file)
300 sub read_opts_file
301 {
302 local @rv;
303 local $file = $_[0];
304 if ($file !~ /^\// && $file !~ /\|\s*$/) {
305         local @uinfo = getpwnam($remote_user);
306         if (@uinfo) {
307                 $file = "$uinfo[7]/$file";
308                 }
309         }
310 open(FILE, $file);
311 while(<FILE>) {
312         s/\r|\n//g;
313         if (/^"([^"]*)"\s+"([^"]*)"$/) {
314                 push(@rv, [ $1, $2 ]);
315                 }
316         elsif (/^"([^"]*)"$/) {
317                 push(@rv, [ $1, $1 ]);
318                 }
319         elsif (/^(\S+)\s+(\S.*)/) {
320                 push(@rv, [ $1, $2 ]);
321                 }
322         else {
323                 push(@rv, [ $_, $_ ]);
324                 }
325         }
326 close(FILE);
327 return @rv;
328 }
329
330 # show_params_inputs(&command, no-quote, editor-mode)
331 sub show_params_inputs
332 {
333 local ($cmd, $noquote, $editor) = @_;
334
335 local $ptable = &ui_columns_start([
336         $text{'edit_name'}, $text{'edit_desc'}, $text{'edit_type'},
337         $noquote ? ( ) : ( $text{'edit_quote'} ),
338         $text{'edit_must'},
339         ], 100, 0, undef, undef);
340 local @a = (@{$cmd->{'args'}}, { });
341 for(my $i=0; $i<@a; $i++) {
342         local @cols;
343         push(@cols, &ui_textbox("name_$i", $a[$i]->{'name'}, 10));
344         push(@cols, &ui_textbox("desc_$i", $a[$i]->{'desc'}, 40));
345         local @opts;
346         for(my $j=0; $text{"edit_type$j"}; $j++) {
347                 next if ($editor &&
348                          ($j == 7 || $j == 8 || $j == 10 || $j == 11));
349                 push(@opts, [ $j, $text{"edit_type$j"} ]);
350                 }
351         push(@cols, &ui_select("type_$i", $a[$i]->{'type'}, \@opts)." ".
352                     &ui_textbox("opts_$i", $a[$i]->{'opts'}, 40));
353         if (!$noquote) {
354                 push(@cols, &ui_yesno_radio("quote_$i",
355                                             int($a[$i]->{'quote'})));
356                 }
357         push(@cols, &ui_yesno_radio("must_$i",
358                                     int($a[$i]->{'must'})));
359         $ptable .= &ui_columns_row(\@cols);
360         }
361
362 $ptable .= &ui_columns_end();
363 print $ptable;
364 }
365
366 # parse_params_inputs(&command)
367 sub parse_params_inputs
368 {
369 local ($cmd) = @_;
370 $cmd->{'args'} = [ ];
371 my ($i, $name);
372 for($i=0; defined($name = $in{"name_$i"}); $i++) {
373         if ($name) {
374                 push(@{$cmd->{'args'}}, { 'name' => $name,
375                                           'desc' => $in{"desc_$i"},
376                                           'type' => $in{"type_$i"},
377                                           'quote' => int($in{"quote_$i"}),
378                                           'must' => int($in{"must_$i"}),
379                                           'opts' => $in{"opts_$i"} });
380                 }
381         }
382 }
383
384 # set_parameter_envs(&command, command-str, &uinfo, [set-in], [skip-menu-check])
385 # Sets $ENV variables based on parameter inputs, and returns the list of
386 # environment variable commands, the export commands, the command string,
387 # and the command string to display.
388 sub set_parameter_envs
389 {
390 local ($cmd, $str, $uinfo, $setin, $skipfound) = @_;
391 $setin ||= \%in;
392 local $displaystr = $str;
393 local ($env, $export, @vals);
394 foreach my $a (@{$cmd->{'args'}}) {
395         my $n = $a->{'name'};
396         my $rv;
397         if ($a->{'type'} == 0 || $a->{'type'} == 5 ||
398             $a->{'type'} == 6 || $a->{'type'} == 8) {
399                 $rv = $setin->{$n};
400                 }
401         elsif ($a->{'type'} == 11) {
402                 $rv = $setin->{$n};
403                 $rv =~ s/\r//g;
404                 $rv =~ s/\n/ /g;
405                 }
406         elsif ($a->{'type'} == 1 || $a->{'type'} == 2) {
407                 (@u = getpwnam($setin->{$n})) || &error($text{'run_euser'});
408                 $rv = $a->{'type'} == 1 ? $setin->{$n} : $u[2];
409                 }
410         elsif ($a->{'type'} == 3 || $a->{'type'} == 4) {
411                 (@g = getgrnam($setin->{$n})) || &error($text{'run_egroup'});
412                 $rv = $a->{'type'} == 3 ? $setin->{$n} : $g[2];
413                 }
414         elsif ($a->{'type'} == 7) {
415                 $rv = $setin->{$n} ? $a->{'opts'} : "";
416                 }
417         elsif ($a->{'type'} == 9) {
418                 local $found;
419                 foreach my $l (&read_opts_file($a->{'opts'})) {
420                         $found++ if ($l->[0] eq $setin->{$n});
421                         }
422                 $found || $skipfound || &error($text{'run_eopt'});
423                 $rv = $setin->{$n};
424                 }
425         elsif ($a->{'type'} == 10) {
426                 $setin->{$n} || &error($text{'run_eupload'});
427                 if ($setin->{$n."_filename"} =~ /([^\/\\]+$)/ && $1) {
428                         $rv = &transname("$1");
429                         }
430                 else {
431                         $rv = &transname();
432                         }
433                 &open_tempfile(TEMP, ">$rv");
434                 &print_tempfile(TEMP, $setin->{$n});
435                 &close_tempfile(TEMP);
436                 chown($uinfo->[2], $uinfo->[3], $rv);
437                 push(@unlink, $rv);
438                 }
439         elsif ($a->{'type'} == 12 || $a->{'type'} == 13 || $a->{'type'} == 14) {
440                 local @vals;
441                 if ($a->{'type'} == 14) {
442                         @vals = split(/\r?\n/, $setin->{$n});
443                         }
444                 else {
445                         @vals = split(/\0/, $setin->{$n});
446                         }
447                 local @opts = &read_opts_file($a->{'opts'});
448                 foreach my $v (@vals) {
449                         local $found;
450                         foreach my $l (@opts) {
451                                 $found++ if ($l->[0] eq $v);
452                                 }
453                         $found || $skipfound || &error($text{'run_eopt'});
454                         }
455                 $rv = join(" ", @vals);
456                 }
457         elsif ($a->{'type'} == 15) {
458                 $rv = $setin->{$n."_year"}."-".
459                       $setin->{$n."_month"}."-".
460                       $setin->{$n."_day"};
461                 }
462         if ($rv eq '' && $a->{'must'} && $a->{'type'} != 7) {
463                 &error(&text('run_emust', $a->{'desc'}));
464                 }
465         $ENV{$n} = $rv;
466         $env .= "$n='$rv'\n";
467         $export .= " $n";
468         if ($a->{'quote'}) {
469                 $str =~ s/\$$n/"\$$n"/g;
470                 $displaystr =~ s/\$$n/"$rv"/g;
471                 }
472         else {
473                 $displaystr =~ s/\$$n/$rv/g;
474                 }
475         push(@vals, $rv);
476         }
477 return ($env, $export, $str, $displaystr, \@vals);
478 }
479
480 # list_dbi_drivers()
481 # Returns a list of DBI driver details, which are actually installed
482 sub list_dbi_drivers
483 {
484 local @rv = ( { 'name' => 'MySQL',
485                 'driver' => 'mysql',
486                 'dbparam' => 'database' },
487               { 'name' => 'PostgreSQL',
488                 'driver' => 'Pg',
489                 'dbparam' => 'dbname' },
490             );
491 @rv = grep { eval "use DBD::$_->{'driver'}"; !$@ } @rv;
492 return @rv;
493 }
494
495 # list_servers()
496 # Returns a list of servers that a command can run on
497 sub list_servers
498 {
499 if (&foreign_installed("servers")) {
500         &foreign_require("servers", "servers-lib.pl");
501         @servers = grep { $_->{'user'} } &servers::list_servers();
502         if (@servers) {
503                 return ( { 'id' => 0, 'desc' => $text{'edit_this'} }, @servers);
504                 }
505         }
506 return ( { 'id' => 0, 'desc' => $text{'edit_this'} } );
507 }
508
509 # execute_custom_command(&command, environment, exports, string, print-output)
510 # Runs some command, and returns the bytes, output and timeout flag
511 sub execute_custom_command
512 {
513 local ($cmd, $env, $export, $str, $print) = @_;
514 &foreign_require("proc", "proc-lib.pl");
515
516 &clean_environment() if ($cmd->{'clear'});
517 local $got;
518 local $outtemp = &transname();
519 open(OUTTEMP, ">$outtemp");
520 local $fh = $print ? STDOUT : *OUTTEMP;
521 if ($cmd->{'su'}) {
522         local $temp = &transname();
523         &open_tempfile(TEMP, ">$temp");
524         &print_tempfile(TEMP, "#!/bin/sh\n");
525         &print_tempfile(TEMP, $env);
526         &print_tempfile(TEMP, "export $export\n") if ($export);
527         &print_tempfile(TEMP, "$str\n");
528         &close_tempfile(TEMP);
529         chmod(0755, $temp);
530         $got = &proc::safe_process_exec(
531                              &command_as_user($user, 1, $temp), 0, 0,
532                              $fh, undef, !$cmd->{'raw'} && !$cmd->{'format'}, 0,
533                              $cmd->{'timeout'});
534         unlink($temp);
535         }
536 else {
537         $got = &proc::safe_process_exec(
538                              $str, $user_info[2], undef, $fh, undef,
539                              !$cmd->{'raw'} && !$cmd->{'format'}, 0,
540                              $cmd->{'timeout'});
541         }
542 &reset_environment() if ($cmd->{'clear'});
543 close(OUTTEMP);
544 local $rv = &read_file_contents($outtemp);
545 unlink($outtemp);
546 return ($got, $rv, $proc::safe_process_exec_timeout ? 1 : 0);
547 }
548
549 # show_parameter_input(&arg, formno)
550 # Returns HTML for a parameter input
551 sub show_parameter_input
552 {
553 local ($a, $form) = @_;
554 local $n = $a->{'name'};
555 local $v = $a->{'opts'};
556 if ($a->{'type'} != 9 && $a->{'type'} != 12 &&
557     $a->{'type'} != 13 && $a->{'type'} != 14) {
558         if ($v =~ /^"(.*)"$/ || $v =~ /^'(.*)'$/) {
559                 # Quoted default
560                 $v = $1;
561                 }
562         elsif ($v =~ /^(.*)\s*\|$/ && $config{'params_cmd'}) {
563                 # Command to run
564                 $v = &backquote_command("$1 2>/dev/null </dev/null");
565                 if ($a->{'type'} != 11) {
566                         $v =~ s/[\r\n]+$//;
567                         }
568                 }
569         elsif ($v =~ /^\// && $config{'params_file'}) {
570                 # File to read
571                 $v = &read_file_contents($v);
572                 if ($a->{'type'} != 11) {
573                         $v =~ s/[\r\n]+$//;
574                         }
575                 }
576         }
577 if ($a->{'type'} == 0) {
578         return &ui_textbox($n, $v, 30);
579         }
580 elsif ($a->{'type'} == 1 || $a->{'type'} == 2) {
581         return &ui_user_textbox($n, $v, $form);
582         }
583 elsif ($a->{'type'} == 3 || $a->{'type'} == 4) {
584         return &ui_group_textbox($n, $v, $form);
585         }
586 elsif ($a->{'type'} == 5 || $a->{'type'} == 6) {
587         return &ui_textbox($n, $v, 30)." ".
588                &file_chooser_button($n, $a->{'type'}-5, $form);
589         }
590 elsif ($a->{'type'} == 7) {
591         return &ui_yesno_radio($n, $v =~ /true|yes|1/ ? 1 : 0);
592         }
593 elsif ($a->{'type'} == 8) {
594         return &ui_password($n, $v, 30);
595         }
596 elsif ($a->{'type'} == 9) {
597         return &ui_select($n, undef, [ &read_opts_file($a->{'opts'}) ]);
598         }
599 elsif ($a->{'type'} == 10) {
600         return &ui_upload($n, 30);
601         }
602 elsif ($a->{'type'} == 11) {
603         return &ui_textarea($n, $v, 4, 30);
604         }
605 elsif ($a->{'type'} == 12) {
606         return &ui_select($n, undef, [ &read_opts_file($a->{'opts'}) ],
607                           5, 1);
608         }
609 elsif ($a->{'type'} == 13) {
610         my @opts = &read_opts_file($a->{'opts'});
611         return &ui_select($n, undef, \@opts, scalar(@opts), 1);
612         }
613 elsif ($a->{'type'} == 14) {
614         my @opts = &read_opts_file($a->{'opts'});
615         return &ui_multi_select($n, [ ], \@opts, 5);
616         }
617 elsif ($a->{'type'} == 15) {
618         my ($year, $month, $day) = split(/\-/, $v);
619         return &ui_date_input($day, $month, $year,
620                               $n."_day", $n."_month", $n."_year")."&nbsp;".
621                &date_chooser_button($n."_day", $n."_month", $n."_year");
622         }
623 else {
624         return "Unknown parameter type $a->{'type'}";
625         }
626 }
627
628
629 1;