Various changes, including changes in 2.1.1 and 2.1.2 releases.
[BackupPC.git] / lib / BackupPC / CGI / EditConfig.pm
1 #============================================================= -*-perl-*-
2 #
3 # BackupPC::CGI::EditConfig package
4 #
5 # DESCRIPTION
6 #
7 #   This module implements the EditConfig action for the CGI interface.
8 #
9 # AUTHOR
10 #   Craig Barratt  <cbarratt@users.sourceforge.net>
11 #
12 # COPYRIGHT
13 #   Copyright (C) 2004  Craig Barratt
14 #
15 #   This program is free software; you can redistribute it and/or modify
16 #   it under the terms of the GNU General Public License as published by
17 #   the Free Software Foundation; either version 2 of the License, or
18 #   (at your option) any later version.
19 #
20 #   This program is distributed in the hope that it will be useful,
21 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
22 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
23 #   GNU General Public License for more details.
24 #
25 #   You should have received a copy of the GNU General Public License
26 #   along with this program; if not, write to the Free Software
27 #   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
28 #
29 #========================================================================
30 #
31 # Version 2.1.0beta2pl1, released 30 May 2004.
32 #
33 # See http://backuppc.sourceforge.net.
34 #
35 #========================================================================
36
37 package BackupPC::CGI::EditConfig;
38
39 use strict;
40 use BackupPC::CGI::Lib qw(:all);
41 use BackupPC::Config::Meta qw(:all);
42 use BackupPC::Storage;
43 use Data::Dumper;
44
45 our %ConfigMenu = (
46     server => {
47         text  => "Server",
48         param => [
49             {text => "General Parameters"},
50             {name => "ServerHost"},
51             {name => "BackupPCUser"},
52             {name => "BackupPCUserVerify"},
53             {name => "MaxOldLogFiles"},
54             {name => "TrashCleanSleepSec"},
55
56             {text => "Wakeup Schedule"},
57             {name => "WakeupSchedule"},
58
59             {text => "Concurrent Jobs"},
60             {name => "MaxBackups"},
61             {name => "MaxUserBackups"},
62             {name => "MaxPendingCmds"},
63             {name => "MaxBackupPCNightlyJobs"},
64             {name => "BackupPCNightlyPeriod"},
65
66             {text => "Pool Filesystem Limits"},
67             {name => "DfCmd"},
68             {name => "DfMaxUsagePct"},
69             {name => "HardLinkMax"},
70
71             {text => "Other Parameters"},
72             {name => "UmaskMode"},
73             {name => "MyPath"},
74             {name => "DHCPAddressRanges"},
75             {name => "PerlModuleLoad"},
76             {name => "ServerInitdPath"},
77             {name => "ServerInitdStartCmd"},
78
79             {text => "Remote Apache Settings"},
80             {name => "ServerPort"},
81             {name => "ServerMesgSecret"},
82
83             {text => "Program Paths"},
84             {name => "SshPath"},
85             {name => "NmbLookupPath"},
86             {name => "PingPath"},
87             {name => "DfPath"},
88             {name => "SplitPath"},
89             {name => "ParPath"},
90             {name => "CatPath"},
91             {name => "GzipPath"},
92             {name => "Bzip2Path"},
93
94             {text => "Install Paths"},
95             {name => "CgiDir"},
96             {name => "InstallDir"},
97         ],
98     },
99     email => {
100         text  => "Email",
101         param => [
102             {text => "Email settings"},
103             {name => "SendmailPath"},
104             {name => "EMailNotifyMinDays"},
105             {name => "EMailFromUserName"},
106             {name => "EMailAdminUserName"},
107             {name => "EMailUserDestDomain"},
108
109             {text => "Email User Messages"},
110             {name => "EMailNoBackupEverSubj"},
111             {name => "EMailNoBackupEverMesg"},
112             {name => "EMailNotifyOldBackupDays"},
113             {name => "EMailNoBackupRecentSubj"},
114             {name => "EMailNoBackupRecentMesg"},
115             {name => "EMailNotifyOldOutlookDays"},
116             {name => "EMailOutlookBackupSubj"},
117             {name => "EMailOutlookBackupMesg"},
118         ],
119     },
120     cgi => {
121         text => "CGI",
122         param => [
123             {text => "Admin Privileges"},
124             {name => "CgiAdminUserGroup"},
125             {name => "CgiAdminUsers"},
126
127             {text => "Config Editing"},
128             {name => "CgiUserConfigEdit"},
129
130             {text => "Page Rendering"},
131             {name => "Language"},
132             {name => "CgiNavBarAdminAllHosts"},
133             {name => "CgiSearchBoxEnable"},
134             {name => "CgiNavBarLinks"},
135             {name => "CgiStatusHilightColor"},
136             {name => "CgiDateFormatMMDD"},
137             {name => "CgiHeaders"},
138             {name => "CgiExt2ContentType"},
139             {name => "CgiCSSFile"},
140
141             {text => "Paths"},
142             {name => "CgiURL"},
143             {name => "CgiImageDir"},
144             {name => "CgiImageDirURL"},
145
146             {text => "User URLs"},
147             {name => "CgiUserHomePageCheck"},
148             {name => "CgiUserUrlCreate"},
149
150         ],
151     },
152     xfer => {
153         text => "Xfer",
154         param => [
155             {text => "Xfer Settings"},
156             {name => "XferMethod", onchangeSubmit => 1},
157             {name => "XferLogLevel"},
158
159             {text => "Smb Settings",
160                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
161             {name => "SmbShareName",
162                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
163             {name => "SmbShareUserName",
164                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
165             {name => "SmbSharePasswd",
166                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
167
168             {text => "Tar Settings",
169                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
170             {name => "TarShareName",
171                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
172
173             {text => "Rsync Settings",
174                 visible => sub { return $_[0]->{XferMethod} eq "rsync"; } },
175             {text => "Rsyncd Settings",
176                 visible => sub { return $_[0]->{XferMethod} eq "rsyncd"; } },
177             {name => "RsyncShareName",
178                 visible => sub { return $_[0]->{XferMethod} =~ /rsync/; } },
179             {name => "RsyncdPasswd",
180                 visible => sub { return $_[0]->{XferMethod} eq "rsyncd"; } },
181             {name => "RsyncdAuthRequired",
182                 visible => sub { return $_[0]->{XferMethod} eq "rsyncd"; } },
183             {name => "RsyncCsumCacheVerifyProb",
184                 visible => sub { return $_[0]->{XferMethod} =~ /rsync/; } },
185
186             {text => "Archive Settings",
187                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
188             {name => "ArchiveDest",
189                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
190             {name => "ArchiveComp",
191                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
192             {name => "ArchivePar",
193                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
194             {name => "ArchiveSplit",
195                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
196
197             {text => "Include/Exclude",
198                 visible => sub { return $_[0]->{XferMethod} ne "archive"; } },
199             {name => "BackupFilesOnly",
200                 visible => sub { return $_[0]->{XferMethod} ne "archive"; } },
201             {name => "BackupFilesExclude",
202                 visible => sub { return $_[0]->{XferMethod} ne "archive"; } },
203
204             {text => "Smb Paths/Commands",
205                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
206             {name => "SmbClientPath",
207                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
208             {name => "SmbClientFullCmd",
209                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
210             {name => "SmbClientIncrCmd",
211                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
212             {name => "SmbClientRestoreCmd",
213                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
214
215             {text => "Tar Paths/Commands",
216                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
217             {name => "TarClientPath",
218                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
219             {name => "TarClientCmd",
220                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
221             {name => "TarFullArgs",
222                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
223             {name => "TarIncrArgs",
224                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
225             {name => "TarClientRestoreCmd",
226                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
227
228             {text => "Rsync Paths/Commands/Args",
229                 visible => sub { return $_[0]->{XferMethod} eq "rsync"; } },
230             {text => "Rsyncd Port/Args",
231                 visible => sub { return $_[0]->{XferMethod} eq "rsyncd"; } },
232             {name => "RsyncClientPath",
233                 visible => sub { return $_[0]->{XferMethod} eq "rsync"; } },
234             {name => "RsyncClientCmd",
235                 visible => sub { return $_[0]->{XferMethod} eq "rsync"; } },
236             {name => "RsyncClientRestoreCmd",
237                 visible => sub { return $_[0]->{XferMethod} eq "rsync"; } },
238             {name => "RsyncdClientPort",
239                 visible => sub { return $_[0]->{XferMethod} eq "rsyncd"; } },
240             {name => "RsyncArgs",
241                 visible => sub { return $_[0]->{XferMethod} =~ /rsync/; } },
242             {name => "RsyncRestoreArgs",
243                 visible => sub { return $_[0]->{XferMethod} =~ /rsync/; } },
244
245             {text => "Archive Paths/Commands",
246                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
247             {name => "ArchiveClientCmd",
248                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
249
250         ],
251     },
252     schedule => {
253         text => "Schedule",
254         param => [
255             {text => "Full Backups"},
256             {name => "FullPeriod"},
257             {name => "FullKeepCnt"},
258             {name => "FullKeepCntMin"},
259             {name => "FullAgeMax"},
260
261             {text => "Incremental Backups"},
262             {name => "IncrPeriod"},
263             {name => "IncrKeepCnt"},
264             {name => "IncrKeepCntMin"},
265             {name => "IncrAgeMax"},
266             {name => "IncrFill"},
267
268             {text => "Blackouts"},
269             {name => "BlackoutBadPingLimit"},
270             {name => "BlackoutGoodCnt"},
271             {name => "BlackoutPeriods"},
272
273             {text => "Other"},
274             {name => "PartialAgeMax"},
275             {name => "RestoreInfoKeepCnt"},
276             {name => "ArchiveInfoKeepCnt"},
277             {name => "BackupZeroFilesIsFatal"},
278         ],
279     },
280     backup => {
281         text => "Backup Settings",
282         param => [
283             {text => "Client Lookup"},
284             {name => "ClientNameAlias"},
285             {name => "NmbLookupCmd"},
286             {name => "NmbLookupFindHostCmd"},
287             {name => "FixedIPNetBiosNameCheck"},
288             {name => "PingCmd"},
289             {name => "PingMaxMsec"},
290             
291             {text => "Other"},
292             {name => "ClientTimeout"},
293             {name => "MaxOldPerPCLogFiles"},
294             {name => "CompressLevel"},
295
296             {text => "User Commands"},
297             {name => "DumpPreUserCmd"},
298             {name => "DumpPostUserCmd"},
299             {name => "DumpPreShareCmd"},
300             {name => "DumpPostShareCmd"},
301             {name => "RestorePreUserCmd"},
302             {name => "RestorePostUserCmd"},
303             {name => "ArchivePreUserCmd"},
304             {name => "ArchivePostUserCmd"},
305         ],
306     },
307 );
308
309 sub action
310 {
311     my $pc_dir = "$TopDir/pc";
312     my($content, $contentHidden, $newConf, $override, $mainConf, $hostConf);
313     my $errors = {};
314
315     my $host = $In{host};
316     my $menu = $In{menu} || "server";
317     my $hosts_path = $Hosts;
318     my $config_path = $host eq "" ? "$TopDir/conf/config.pl"
319                                   : "$TopDir/pc/$host/config.pl";
320
321     my $Privileged = CheckPermission();
322     my $userHost = 1 if ( $Privileged && !$PrivAdmin && defined($host) );
323
324     if ( !$Privileged ) {
325         ErrorExit(eval("qq{$Lang->{Only_privileged_users_can_edit_config_files}}"));
326     }
327
328     if ( defined($In{menu}) || $In{editAction} eq "Save" ) {
329         $errors = errorCheck();
330         if ( %$errors ) {
331             #
332             # If there are errors, then go back to the same menu
333             #
334             $In{editAction} = "";
335             $In{newMenu} = "";
336         }
337         ($newConf, $override) = inputParse($bpc, $userHost);
338         $override = undef if ( $host eq "" );
339
340         #
341         # Copy all the orig_ input parameters
342         #
343         foreach my $var ( keys(%In) ) {
344             next if ( $var !~ /^orig_/ );
345             $contentHidden .= <<EOF;
346 <input type="hidden" name="$var" value="${EscHTML($In{$var})}">
347 EOF
348         }
349     } else {
350         #
351         # First time: pick up the current config settings
352         #
353         $mainConf = $bpc->ConfigDataRead();
354         if ( $host ne "" ) {
355             $hostConf = $bpc->ConfigDataRead($host);
356             $override = {};
357             foreach my $param ( keys(%$hostConf) ) {
358                 $override->{$param} = 1;
359             }
360         } else {
361             $hostConf = {};
362         }
363         $newConf = { %$mainConf, %$hostConf };
364
365         #
366         # Emit all the original config settings
367         #
368         my $doneParam = {};
369         foreach my $param ( keys(%ConfigMeta) ) {
370             next if ( $doneParam->{$param} );
371             next if ( $userHost && !$bpc->{Conf}{CgiUserConfigEdit}{$param} );
372             $contentHidden .= fieldHiddenBuild($ConfigMeta{$param},
373                                     $param,
374                                     $mainConf->{$param},
375                                     "orig",
376                                 );
377             $doneParam->{$param} = 1;
378         }
379
380     }
381
382     if ( $In{editAction} ne "Save" && $In{newMenu} ne ""
383                     && defined($ConfigMenu{$In{newMenu}}) ) {
384         $menu = $In{newMenu};
385     }
386
387     my %menuDisable;
388     if ( $userHost ) {
389         #
390         # For a non-admin user editing the host config, we need to
391         # figure out which subsets of the menu tree will be visible,
392         # based on what is enabled
393         #
394         foreach my $m ( keys(%ConfigMenu) ) {
395             my $enabled = 0;
396             my $text = -1;
397             my $n = 0;
398             my @mask = ();
399
400             foreach my $paramInfo ( @{$ConfigMenu{$m}{param}} ) {
401                 my $param = $paramInfo->{name};
402                 if ( defined($paramInfo->{text}) ) {
403                     $text = $n;
404                     $mask[$text] = 1;
405                 } else {
406                     if ( $bpc->{Conf}{CgiUserConfigEdit}{$param} ) {
407                         $mask[$text] = 0 if ( $text >= 0 );
408                         $mask[$n] = 0;
409                         $enabled = 1;
410                     } else {
411                         $mask[$n] = 1;
412                     }
413                 }
414                 $n++;
415             }
416             $menuDisable{$m}{mask} = \@mask;
417             $menuDisable{$m}{top}  = !$enabled;
418         }
419         if ( $menuDisable{$menu}{top} ) {
420             #
421             # Find an enabled menu if the current menu is empty
422             #
423             foreach my $m ( sort(keys(%menuDisable)) ) {
424                 if ( !$menuDisable{$m}{top} ) {
425                     $menu = $m;
426                     last;
427                 }
428             }
429         }
430     }
431
432     my $groupText;
433     foreach my $m ( keys(%ConfigMenu) ) {
434         next if ( $menuDisable{$m}{top} );
435         my $text = $ConfigMenu{$m}{text};
436         if ( $m eq $menu ) {
437             $groupText .= <<EOF;
438 <td bgcolor="grey"><a href="javascript:menuSubmit('$m')"><b>$text</b></a></td>
439 EOF
440         } else {
441             $groupText .= <<EOF;
442 <td><a href="javascript:menuSubmit('$m')">$text</a></td>
443 EOF
444         }
445     }
446
447     if ( $host eq "" ) {
448         $content .= <<EOF;
449 ${h1("Main Configuration Editor")}
450 EOF
451     } else {
452         $content .= <<EOF;
453 ${h1("Host $host Configuration Editor")}
454 <p>
455 Note: Check Override if you want to modify a value specific to this host.
456 EOF
457     }
458
459     my $saveDisplay = "block";
460     $saveDisplay = "none" if ( !$In{modified} );
461     $content .= <<EOF;
462 <table border="0" cellpadding="2">
463 <tr>$groupText</tr>
464 <tr>
465 <form method="post" name="form1" action="$MyURL">
466 <input type="hidden" name="host" value="$host">
467 <input type="hidden" name="menu" value="$menu">
468 <input type="hidden" name="newMenu" value="">
469 <input type="hidden" name="modified" value="$In{modified}">
470 <input type="hidden" name="deleteVar" value="">
471 <input type="hidden" name="insertVar" value="">
472 <input type="hidden" name="addVar" value="">
473 <input type="hidden" name="action" value="editConfig">
474 <input type="submit" style="display: $saveDisplay" name="editAction" value="Save">
475 $contentHidden
476
477 <script language="javascript" type="text/javascript">
478 <!--
479
480     function deleteSubmit(varName)
481     {
482         document.form1.deleteVar.value = varName;
483         document.form1.modified.value = 1;
484         document.form1.submit();
485         return;
486     }
487
488     function insertSubmit(varName)
489     {
490         document.form1.insertVar.value = varName;
491         document.form1.modified.value = 1;
492         document.form1.submit();
493         return;
494     }
495
496     function addSubmit(varName, checkKey)
497     {
498         if ( checkKey && document.form1.addVarKey.value == "" ) {
499             alert("New key must be non-empty");
500             return;
501         }
502         document.form1.addVar.value = varName;
503         document.form1.modified.value = 1;
504         document.form1.submit();
505         return;
506     }
507
508     function menuSubmit(menuName)
509     {
510         document.form1.newMenu.value = menuName;
511         document.form1.submit();
512     }
513
514     function varChange(varName)
515     {
516         document.form1.editAction.style.display = "block";
517         document.form1.modified.value = 1;
518     }
519
520     function checkboxChange(varName)
521     {
522         document.form1.editAction.style.display = "block";
523         document.form1.modified.value = 1;
524         // Do nothing if the checkbox is now set
525         if ( eval("document.form1.override_" + varName + ".checked") ) {
526             return false;
527         }
528         var allVars = {};
529         var varRE  = new RegExp("^v_(" + varName + ".*)");
530         var origRE = new RegExp("^orig_(" + varName + ".*)");
531         for ( var i = 0 ; i < document.form1.elements.length ; i++ ) {
532             var e = document.form1.elements[i];
533             var re;
534             if ( (re = varRE.exec(e.name)) != null ) {
535                 if ( allVars[re[1]] == null ) {
536                     allVars[re[1]] = 0;
537                 }
538                 allVars[re[1]]++;
539                 //debugMsg("found v_ match with " + re[1]);
540                 //debugMsg("allVars[" + re[1] + "] = " + allVars[re[1]]);
541             } else if ( (re = origRE.exec(e.name)) != null ) {
542                 if ( allVars[re[1]] == null ) {
543                     allVars[re[1]] = 0;
544                 }
545                 allVars[re[1]]--;
546                 //debugMsg("allVars[" + re[1] + "] = " + allVars[re[1]]);
547             }
548         }
549         var sameShape = 1;
550         for ( v in allVars ) {
551             if ( allVars[v] != 0 ) {
552                 //debugMsg("Not the same shape because of " + v);
553                 sameShape = 0;
554             }
555         }
556         if ( sameShape ) {
557             for ( v in allVars ) {
558                 //debugMsg("setting " + v);
559                 eval("document.form1.v_" + v + ".value = document.form1.orig_" + v + ".value");
560             }
561             return true;
562         } else {
563             document.form1.submit();
564             return false;
565         }
566     }
567
568     function checkboxSet(varName)
569     {
570         document.form1.editAction.style.display = "block";
571         document.form1.modified.value = 1;
572         eval("document.form1.override_" + varName + ".checked = 1;");
573         return false;
574     }
575
576     var debugCounter = 0;
577     function debugMsg(msg)
578     {
579         debugCounter++;
580         var t = document.createTextNode(debugCounter + ": " + msg);
581         var br = document.createElement("br");
582         var debug = document.getElementById("debug");
583         debug.appendChild(t);
584         debug.appendChild(br);
585     }
586
587     function displayHelp(varName)
588     {
589         var help = document.getElementById("id_" + varName);
590         help.style.display = help.style.display == "block" ? "none" : "block";
591     }
592
593 //-->
594 </script>
595
596 <span id="debug"></span>
597
598 EOF
599
600     $content .= <<EOF;
601 <table border="1" cellspacing="0">
602 EOF
603
604     my $doneParam = {};
605
606     #
607     # There is a special case of the user deleting just the field
608     # that has the error(s).  So if the delete variable is a match
609     # or parent to all the errors then ignore the errors.
610     #
611     if ( $In{deleteVar} ne "" && %$errors > 0 ) {
612         my $matchAll = 1;
613         foreach my $v ( keys(%$errors) ) {
614             if ( $v ne $In{deleteVar} && $v !~ /^\Q$In{deleteVar}_/ ) {
615                 $matchAll = 0;
616                 last;
617             }
618         }
619         $errors = {} if ( $matchAll );
620     }
621
622     my $isError = %$errors;
623
624     if ( !$isError && $In{editAction} eq "Save" ) {
625         my $mesg;
626         if ( $host ne "" ) {
627             $hostConf = $bpc->ConfigDataRead($host) if ( !defined($hostConf) );
628             $mesg = configDiffMesg($host, $hostConf, $newConf);
629             foreach my $param ( %$newConf ) {
630                 $hostConf->{$param} = $newConf->{$param}
631                                 if ( $override->{param} );
632             }
633             $bpc->ConfigDataWrite($host, $hostConf);
634         } else {
635             $mainConf = $bpc->ConfigDataRead() if ( !defined($mainConf) );
636             $mesg = configDiffMesg(undef, $mainConf, $newConf);
637             $mainConf = { %$mainConf, %$newConf };
638             $bpc->ConfigDataWrite(undef, $mainConf);
639         }
640         if ( $mesg ne "" ) {
641             $bpc->ServerConnect();
642             foreach my $str ( split(/\n/, $mesg) ) {
643                 $bpc->ServerMesg($str);
644             }
645         }
646     }
647
648     my @mask = @{$menuDisable{$menu}{mask} || []};
649
650     foreach my $paramInfo ( @{$ConfigMenu{$menu}{param}} ) {
651
652         my $param    = $paramInfo->{name};
653         my $disabled = shift(@mask);
654
655         next if ( $disabled || $menuDisable{$menu}{top} );
656         if ( ref($paramInfo->{visible}) eq "CODE"
657                         && !&{$paramInfo->{visible}}($newConf) ) {
658             next;
659         }
660
661         if ( defined(my $text = $paramInfo->{text}) ) {
662             $content .= <<EOF;
663 <tr><td colspan="2" class="editHeader">$text</td></tr>
664 EOF
665             next;
666         }
667
668         #
669         # TODO: get parameter documentation
670         #
671         my $comment = "";
672         $comment =~ s/\'//g;
673         $comment =~ s/\"//g;
674         $comment =~ s/\n/ /g;
675
676         $doneParam->{$param} = 1;
677
678         $content .= fieldEditBuild($ConfigMeta{$param},
679                                 $param,
680                                 $newConf->{$param},
681                                 $errors,
682                                 0,
683                                 $comment,
684                                 $isError,
685                                 $paramInfo->{onchangeSubmit},
686                                 defined($override) ? $param : undef,
687                                 defined($override) ? $override->{$param} : undef
688                         );
689     }
690
691     #
692     # Emit any remaining errors - should not happen
693     #
694     foreach my $param ( sort(keys(%$errors)) ) {
695         $content .= <<EOF;
696 <tr><td colspan="2" class="border">$errors->{$param}</td></tr>
697 EOF
698         delete($errors->{$param});
699     }
700
701     $content .= <<EOF;
702 </table>
703 EOF
704
705     #
706     # Emit all the remaining editable config settings as hidden values
707     #
708     foreach my $param ( keys(%ConfigMeta) ) {
709         next if ( $doneParam->{$param} );
710         next if ( $userHost && !$bpc->{Conf}{CgiUserConfigEdit}{$param} );
711         $content .= fieldHiddenBuild($ConfigMeta{$param},
712                             $param,
713                             $newConf->{$param},
714                             "v"
715                         );
716         if ( defined($override) ) {
717             $content .= <<EOF;
718 <input type="hidden" name="override_$param" value="$override->{$param}">
719 EOF
720         }
721         $doneParam->{$param} = 1;
722     }
723
724     $content .= <<EOF;
725 </form>
726 </tr>
727 </table>
728 EOF
729
730     Header("Config Edit", $content);
731     Trailer();
732 }
733
734 sub fieldHiddenBuild
735 {
736     my($type, $varName, $varValue, $prefix) = @_;
737     my $content;
738
739     $type = { type => $type } if ( ref($type) ne "HASH" );
740
741     if ( $type->{type} eq "list" ) {
742         $varValue = [] if ( !defined($varValue) );
743         $varValue = [$varValue] if ( ref($varValue) ne "ARRAY" );
744
745         for ( my $i = 0 ; $i < @$varValue ; $i++ ) {
746             $content .= fieldHiddenBuild($type->{child}, "${varName}_$i",
747                                          $varValue->[$i], $prefix);
748         }
749     } elsif ( $type->{type} eq "hash" ) {
750         $varValue = {} if ( ref($varValue) ne "HASH" );
751         my(@order, $childType);
752
753         if ( defined($type->{child}) ) {
754             @order = sort(keys(%{$type->{child}}));
755         } else {
756             @order = sort(keys(%$varValue));
757         }
758
759         foreach my $fld ( @order ) {
760             if ( defined($type->{child}) ) {
761                 $childType = $type->{child}{$fld};
762             } else {
763                 $childType = $type->{childType};
764                 #
765                 # emit list of fields since they are user-defined
766                 # rather than hard-coded
767                 #
768                 $content .= <<EOF;
769 <input type="hidden" name="vflds.$varName" value="${EscHTML($fld)}">
770 EOF
771             }
772             $content .= fieldHiddenBuild($childType, "${varName}_$fld",
773                                          $varValue->{$fld}, $prefix);
774         }
775     } elsif ( $type->{type} eq "shortlist" ) {
776         $varValue = [$varValue] if ( ref($varValue) ne "ARRAY" );
777         $varValue = join(", ", @$varValue);
778         $content .= <<EOF;
779 <input type="hidden" name="${prefix}_$varName" value="${EscHTML($varValue)}">
780 EOF
781     } else {
782         $content .= <<EOF;
783 <input type="hidden" name="${prefix}_$varName" value="${EscHTML($varValue)}">
784 EOF
785     }
786     return $content;
787 }
788
789 sub fieldEditBuild
790 {
791     my($type, $varName, $varValue, $errors, $level, $comment, $isError,
792        $onchangeSubmit, $overrideVar, $overrideSet) = @_;
793
794     my $content;
795     my $size = 50 - 10 * $level;
796     $type = { type => $type } if ( ref($type) ne "HASH" );
797
798     if ( $level == 0 ) {
799         $content .= <<EOF;
800 <tr id="id_$varName" class="optionalComment"><td colspan="2">$comment</td></tr>
801 <tr><td class="border"><a href="javascript: displayHelp('$varName')">$varName</a>
802 EOF
803         if ( defined($overrideVar) ) {
804             my $override_checked = "";
805             if ( !$isError && $In{deleteVar}       =~ /^\Q${varName}_/
806                     || !$isError && $In{insertVar} =~ /^\Q${varName}\E(_|$)/
807                     || !$isError && $In{addVar}    =~ /^\Q${varName}\E(_|$)/ ) {
808                 $overrideSet = 1;
809             }
810             if ( $overrideSet ) {
811                 $override_checked = "checked";
812             }
813             $content .= <<EOF;
814 <br><input type="checkbox" name="override_$varName" $override_checked value="1" onClick="checkboxChange('$varName')">\&nbsp;Override
815 EOF
816         }
817         $content .= "</td>\n";
818     }
819
820     $content .= "<td class=\"border\">\n";
821     if ( $type->{type} eq "list" ) {
822         $varValue = [] if ( !defined($varValue) );
823         $varValue = [$varValue] if ( ref($varValue) ne "ARRAY" );
824         if ( !$isError && $In{deleteVar} =~ /^\Q${varName}_\E(\d+)$/
825                 && $1 < @$varValue ) {
826             #
827             # User deleted entry in this array
828             #
829             splice(@$varValue, $1, 1) if ( @$varValue > 1 || $type->{emptyOk} );
830             $In{deleteVar} = "";
831         }
832         if ( !$isError && $In{insertVar} =~ /^\Q${varName}_\E(\d+)$/
833                 && $1 < @$varValue ) {
834             #
835             # User inserted entry in this array
836             #
837             splice(@$varValue, $1, 0, "")
838                         if ( @$varValue > 1 || $type->{emptyOk} );
839             $In{insertVar} = "";
840         }
841         if ( !$isError && $In{addVar} eq $varName ) {
842             #
843             # User added entry to this array
844             #
845             push(@$varValue, undef);
846             $In{addVar} = "";
847         }
848         $content .= "<table border=\"1\" cellspacing=\"0\">\n";
849
850         for ( my $i = 0 ; $i < @$varValue ; $i++ ) {
851             $content .= "<tr><td class=\"border\">\n";
852             if ( @$varValue > 1 || $type->{emptyOk} ) {
853                 $content .= <<EOF;
854 <input type="button" name="ins_${varName}_$i" value="Insert"
855     onClick="insertSubmit('${varName}_$i')">
856 <input type="button" name="del_${varName}_$i" value="Delete"
857     onClick="deleteSubmit('${varName}_$i')">
858 EOF
859             }
860             $content .= "</td>\n";
861             $content .= fieldEditBuild($type->{child}, "${varName}_$i",
862                                 $varValue->[$i], $errors, $level + 1, undef,
863                                 $isError, $onchangeSubmit,
864                                 $overrideVar, $overrideSet);
865             $content .= "</tr>\n";
866         }
867         $content .= <<EOF;
868 <tr><td class="border"><input type="button" name="add_$varName" value="Add"
869     onClick="addSubmit('$varName')"></td></tr>
870 </table>
871 EOF
872     } elsif ( $type->{type} eq "hash" ) {
873         $content .= "<table border=\"1\" cellspacing=\"0\">\n";
874         $varValue = {} if ( ref($varValue) ne "HASH" );
875
876         if ( !$isError && !$type->{noKeyEdit}
877                         && $In{deleteVar} =~ /^\Q${varName}_\E(\w+)$/ ) {
878             #
879             # User deleted entry in this array
880             #
881             delete($varValue->{$1}) if ( keys(%$varValue) > 1
882                                             || $type->{emptyOk} );
883             $In{deleteVar} = "";
884         }
885         if ( !$isError && !defined($type->{child})
886                         && $In{addVar} eq $varName ) {
887             #
888             # User added entry to this array
889             #
890             $varValue->{$In{addVarKey}} = ""
891                             if ( !defined($varValue->{$In{addVarKey}}) );
892             $In{addVar} = "";
893         }
894         my(@order, $childType);
895
896         if ( defined($type->{child}) ) {
897             @order = sort(keys(%{$type->{child}}));
898         } else {
899             @order = sort(keys(%$varValue));
900         }
901
902         foreach my $fld ( @order ) {
903             $content .= <<EOF;
904 <tr><td class="border">$fld
905 EOF
906             if ( !$type->{noKeyEdit}
907                     && (keys(%$varValue) > 1 || $type->{emptyOk}) ) {
908                 $content .= <<EOF;
909 <input type="submit" name="del_${varName}_$fld" value="Delete"
910         onClick="deleteSubmit('${varName}_$fld')">
911 EOF
912             }
913             if ( defined($type->{child}) ) {
914                 $childType = $type->{child}{$fld};
915             } else {
916                 $childType = $type->{childType};
917                 #
918                 # emit list of fields since they are user-defined
919                 # rather than hard-coded
920                 #
921                 $content .= <<EOF;
922 <input type="hidden" name="vflds.$varName" value="${EscHTML($fld)}">
923 EOF
924             }
925             $content .= "</td>\n";
926             $content .= fieldEditBuild($childType, "${varName}_$fld",
927                             $varValue->{$fld}, $errors, $level + 1, undef,
928                             $isError, $onchangeSubmit,
929                             $overrideVar, $overrideSet);
930             $content .= "</tr>\n";
931         }
932
933         if ( !$type->{noKeyEdit} ) {
934             $content .= <<EOF;
935 <tr><td class="border" colspan="2">
936 New key: <input type="text" name="addVarKey" size="20" maxlength="256" value="">
937 <input type="button" name="add_$varName" value="Add" onClick="addSubmit('$varName', 1)">
938 </td></tr>
939 EOF
940         }
941         $content .= "</table>\n";
942     } else {
943         if ( $isError ) {
944             #
945             # If there was an error, we use the original post values
946             # in %In, rather than the parsed values in $varValue.
947             # This is so that the user's erroneous input is preserved.
948             #
949             $varValue = $In{"v_$varName"} if ( defined($In{"v_$varName"}) );
950         }
951         if ( defined($errors->{$varName}) ) {
952             $content .= <<EOF;
953 $errors->{$varName}<br>
954 EOF
955             delete($errors->{$varName});
956         }
957         my $onChange;
958         if ( defined($overrideVar) ) {
959             $onChange .= "checkboxSet('$overrideVar');";
960         } else {
961             $onChange .= "varChange('$overrideVar');";
962         }
963         if ( $onchangeSubmit ) {
964             $onChange .= "document.form1.submit();";
965         }
966         if ( $onChange ne "" ) {
967             $onChange = " onChange=\"$onChange\"";
968         }
969         if ( $varValue !~ /\n/ &&
970                 ($type->{type} eq "integer"
971                     || $type->{type} eq "string"
972                     || $type->{type} eq "shortlist"
973                     || $type->{type} eq "float") ) {
974             # simple input box
975             if ( $type->{type} eq "shortlist" ) {
976                 $varValue = [$varValue] if ( ref($varValue) ne "ARRAY" );
977                 $varValue = join(", ", @$varValue);
978             }
979             $content .= <<EOF;
980 <input type="text" name="v_$varName" size="$size" maxlength="256" value="${EscHTML($varValue)}"$onChange>
981 EOF
982         } elsif ( $type->{type} eq "boolean" ) {
983             # checkbox
984             my $checked = "checked" if ( $varValue );
985             $content .= <<EOF;
986 <input type="checkbox" name="v_$varName" $checked value="1">
987 EOF
988         } elsif ( $type->{type} eq "select" ) {
989             $content .= <<EOF;
990 <select name="v_$varName"$onChange>
991 EOF
992             foreach my $option ( @{$type->{values}} ) {
993                 my $sel = " selected" if ( $varValue eq $option );
994                 $content .= "<option$sel>$option</option>\n";
995             }
996             $content .= "</select>\n";
997         } else {
998             # multi-line text area - count number of lines
999             my $rowCnt = $varValue =~ tr/\n//;
1000             $rowCnt = 1 if ( $rowCnt < 1 );
1001             $content .= <<EOF;
1002 <textarea name="v_$varName" cols="$size" rows="$rowCnt"$onChange>${EscHTML($varValue)}</textarea>
1003 EOF
1004         }
1005     }
1006     $content .= "</td>\n";
1007     return $content;
1008 }
1009
1010 sub errorCheck
1011 {
1012     my $errors = {};
1013
1014     foreach my $param ( keys(%ConfigMeta) ) {
1015         fieldErrorCheck($ConfigMeta{$param}, $param, $errors);
1016     }
1017     return $errors;
1018 }
1019
1020 sub fieldErrorCheck
1021 {
1022     my($type, $varName, $errors) = @_;
1023
1024     $type = { type => $type } if ( ref($type) ne "HASH" );
1025
1026     if ( $type->{type} eq "list" ) {
1027         for ( my $i = 0 ; ; $i++ ) {
1028             last if ( fieldErrorCheck($type->{child}, "${varName}_$i", $errors) );
1029         }
1030     } elsif ( $type->{type} eq "hash" ) {
1031         my(@order, $childType);
1032         my $ret;
1033
1034         if ( defined($type->{child}) ) {
1035             @order = sort(keys(%{$type->{child}}));
1036         } else {
1037             @order = split(/\0/, $In{"vflds.$varName"});
1038         }
1039         foreach my $fld ( @order ) {
1040             if ( defined($type->{child}) ) {
1041                 $childType = $type->{child}{$fld};
1042             } else {
1043                 $childType = $type->{childType};
1044             }
1045             $ret ||= fieldErrorCheck($childType, "${varName}_$fld", $errors);
1046         }
1047         return $ret;
1048     } else {
1049         return 1 if ( !exists($In{"v_$varName"}) );
1050
1051         if ( $type->{type} eq "integer"
1052                 || $type->{type} eq "boolean" ) {
1053             if ( $In{"v_$varName"} !~ /^-?\d+\s*$/s
1054                             && $In{"v_$varName"} ne "" ) {
1055                 $errors->{$varName} = "Error: $varName must be an integer";
1056             }
1057         } elsif ( $type->{type} eq "float" ) {
1058             if ( $In{"v_$varName"} !~ /^-?\d*(\.\d*)?\s*$/s
1059                             && $In{"v_$varName"} ne "" ) {
1060                 $errors->{$varName}
1061                         = "Error: $varName must be a real-valued number";
1062             }
1063         } elsif ( $type->{type} eq "shortlist" ) {
1064             my @vals = split(/[,\s]+/, $In{"v_$varName"});
1065             for ( my $i = 0 ; $i < @vals ; $i++ ) {
1066                 if ( $type->{child} eq "integer"
1067                         && $vals[$i] !~ /^-?\d+\s*$/s
1068                         && $vals[$i] ne "" ) {
1069                     my $k = $i + 1;
1070                     $errors->{$varName} = "Error: $varName entry $k must"
1071                                         . " be an integer";
1072                 } elsif ( $type->{child} eq "float"
1073                         && $vals[$i] !~ /^-?\d*(\.\d*)?\s*$/s
1074                         && $vals[$i] ne "" ) {
1075                     my $k = $i + 1;
1076                     $errors->{$varName} = "Error: $varName entry $k must"
1077                                         . " be a real-valued number";
1078                 }
1079             }
1080         } elsif ( $type->{type} eq "select" ) {
1081             my $match = 0;
1082             foreach my $option ( @{$type->{values}} ) {
1083                 if ( $In{"v_$varName"} eq $option ) {
1084                     $match = 1;
1085                     last;
1086                 }
1087             }
1088             $errors->{$varName} = "Error: $varName must be a valid option"
1089                             if ( !$match );
1090         } else {
1091             #
1092             # $type->{type} eq "string": no error checking
1093             #
1094         }
1095     }
1096     return 0;
1097 }
1098
1099 sub inputParse
1100 {
1101     my($bpc, $userHost) = @_;
1102     my $conf     = {};
1103     my $override = {};
1104
1105     foreach my $param ( keys(%ConfigMeta) ) {
1106         my $value;
1107         next if ( $userHost && !$bpc->{Conf}{CgiUserConfigEdit}{$param} );
1108         fieldInputParse($ConfigMeta{$param}, $param, \$value);
1109         $conf->{$param}     = $value;
1110         $override->{$param} = $In{"override_$param"};
1111 }
1112     return ($conf, $override);
1113 }
1114
1115 sub fieldInputParse
1116 {
1117     my($type, $varName, $value) = @_;
1118
1119     $type = { type => $type } if ( ref($type) ne "HASH" );
1120
1121     if ( $type->{type} eq "list" ) {
1122         $$value = [];
1123         for ( my $i = 0 ; ; $i++ ) {
1124             my $val;
1125             last if ( fieldInputParse($type->{child}, "${varName}_$i", \$val) );
1126             push(@$$value, $val);
1127         }
1128         $$value = undef if ( $type->{undefIfEmpty} && @$$value == 0 );
1129     } elsif ( $type->{type} eq "hash" ) {
1130         my(@order, $childType);
1131         my $ret;
1132         $$value = {};
1133
1134         if ( defined($type->{child}) ) {
1135             @order = sort(keys(%{$type->{child}}));
1136         } else {
1137             @order = split(/\0/, $In{"vflds.$varName"});
1138         }
1139
1140         foreach my $fld ( @order ) {
1141             my $val;
1142             if ( defined($type->{child}) ) {
1143                 $childType = $type->{child}{$fld};
1144             } else {
1145                 $childType = $type->{childType};
1146             }
1147             $ret ||= fieldInputParse($childType, "${varName}_$fld", \$val);
1148             last if ( $ret );
1149             $$value->{$fld} = $val;
1150         }
1151         return $ret;
1152     } else {
1153         if ( $type->{type} eq "boolean" ) {
1154             $$value = 0 + $In{"v_$varName"};
1155         } elsif ( !exists($In{"v_$varName"}) ) {
1156             return 1;
1157         }
1158
1159         if ( $type->{type} eq "integer" ) {
1160             $$value = 0 + $In{"v_$varName"};
1161         } elsif ( $type->{type} eq "float" ) {
1162             $$value = 0 + $In{"v_$varName"};
1163         } elsif ( $type->{type} eq "shortlist" ) {
1164             $$value = [split(/[,\s]+/, $In{"v_$varName"})];
1165             if ( $type->{child} eq "float"
1166                     || $type->{child} eq "integer"
1167                     || $type->{child} eq "boolean" ) {
1168                 foreach ( @$$value ) {
1169                     $_ += 0;
1170                 }
1171             }
1172         } else {
1173             $$value = $In{"v_$varName"};
1174         }
1175         $$value = undef if ( $type->{undefIfEmpty} && $$value eq "" );
1176     }
1177     return 0;
1178 }
1179
1180 sub configDiffMesg
1181 {
1182     my($host, $oldConf, $newConf) = @_;
1183     my $mesg;
1184     my $conf;
1185
1186     if ( $host ne "" ) {
1187         $conf = "host $host config";
1188     } else {
1189         $conf = "main config";
1190     }
1191
1192     foreach my $p ( keys(%ConfigMeta) ) {
1193         if ( !exists($oldConf->{$p}) && !exists($newConf->{$p}) ) {
1194             next;
1195         } elsif ( exists($oldConf->{$p}) && !exists($newConf->{$p}) ) {
1196             $mesg .= "log Deleted $p from $conf\n";
1197         } elsif ( !exists($oldConf->{$p}) && exists($newConf->{$p}) ) {
1198             my $dump = Data::Dumper->new([$newConf->{$p}]);
1199             $dump->Indent(0);
1200             $dump->Sortkeys(1);
1201             $dump->Terse(1);
1202             my $value = $dump->Dump;
1203             $mesg .= "log Added $p to $conf, set to $value\n";
1204         } else {
1205             my $dump = Data::Dumper->new([$newConf->{$p}]);
1206             $dump->Indent(0);
1207             $dump->Sortkeys(1);
1208             $dump->Terse(1);
1209             my $valueNew = $dump->Dump;
1210
1211             my $v = $oldConf->{$p};
1212             if ( ref($newConf->{$p}) eq "ARRAY" && ref($v) eq "" ) {
1213                 $v = [$v];
1214             }
1215             $dump = Data::Dumper->new([$v]);
1216             $dump->Indent(0);
1217             $dump->Sortkeys(1);
1218             $dump->Terse(1);
1219             my $valueOld = $dump->Dump;
1220
1221             $mesg .= "log Changed $p in $conf to $valueNew from $valueOld\n"
1222                                     if ( $valueOld ne $valueNew );
1223         }
1224     }
1225     return $mesg;
1226 }
1227
1228 1;