92f9cce2f7d4da17d8fd45c974519694dd1c138f
[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         ErrorExit("Only_privileged_users_can_edit_config_files");
327     }
328
329     if ( defined($In{menu}) || $In{editAction} eq "Save" ) {
330         $errors = errorCheck();
331         if ( %$errors ) {
332             #
333             # If there are errors, then go back to the same menu
334             #
335             $In{editAction} = "";
336             $In{newMenu} = "";
337         }
338         ($newConf, $override) = inputParse($bpc, $userHost);
339         $override = undef if ( $host eq "" );
340
341         #
342         # Copy all the orig_ input parameters
343         #
344         foreach my $var ( keys(%In) ) {
345             next if ( $var !~ /^orig_/ );
346             $contentHidden .= <<EOF;
347 <input type="hidden" name="$var" value="${EscHTML($In{$var})}">
348 EOF
349         }
350     } else {
351         #
352         # First time: pick up the current config settings
353         #
354         $mainConf = $bpc->ConfigDataRead();
355         if ( $host ne "" ) {
356             $hostConf = $bpc->ConfigDataRead($host);
357             $override = {};
358             foreach my $param ( keys(%$hostConf) ) {
359                 $override->{$param} = 1;
360             }
361         } else {
362             $hostConf = {};
363         }
364         $newConf = { %$mainConf, %$hostConf };
365
366         #
367         # Emit all the original config settings
368         #
369         my $doneParam = {};
370         foreach my $param ( keys(%ConfigMeta) ) {
371             next if ( $doneParam->{$param} );
372             next if ( $userHost && !$bpc->{Conf}{CgiUserConfigEdit}{$param} );
373             $contentHidden .= fieldHiddenBuild($ConfigMeta{$param},
374                                     $param,
375                                     $mainConf->{$param},
376                                     "orig",
377                                 );
378             $doneParam->{$param} = 1;
379         }
380
381     }
382
383     if ( $In{editAction} ne "Save" && $In{newMenu} ne ""
384                     && defined($ConfigMenu{$In{newMenu}}) ) {
385         $menu = $In{newMenu};
386     }
387
388     my %menuDisable;
389     if ( $userHost ) {
390         #
391         # For a non-admin user editing the host config, we need to
392         # figure out which subsets of the menu tree will be visible,
393         # based on what is enabled
394         #
395         foreach my $m ( keys(%ConfigMenu) ) {
396             my $enabled = 0;
397             my $text = -1;
398             my $n = 0;
399             my @mask = ();
400
401             foreach my $paramInfo ( @{$ConfigMenu{$m}{param}} ) {
402                 my $param = $paramInfo->{name};
403                 if ( defined($paramInfo->{text}) ) {
404                     $text = $n;
405                     $mask[$text] = 1;
406                 } else {
407                     if ( $bpc->{Conf}{CgiUserConfigEdit}{$param} ) {
408                         $mask[$text] = 0 if ( $text >= 0 );
409                         $mask[$n] = 0;
410                         $enabled = 1;
411                     } else {
412                         $mask[$n] = 1;
413                     }
414                 }
415                 $n++;
416             }
417             $menuDisable{$m}{mask} = \@mask;
418             $menuDisable{$m}{top}  = !$enabled;
419         }
420         if ( $menuDisable{$menu}{top} ) {
421             #
422             # Find an enabled menu if the current menu is empty
423             #
424             foreach my $m ( sort(keys(%menuDisable)) ) {
425                 if ( !$menuDisable{$m}{top} ) {
426                     $menu = $m;
427                     last;
428                 }
429             }
430         }
431     }
432
433     my $groupText;
434     foreach my $m ( keys(%ConfigMenu) ) {
435         next if ( $menuDisable{$m}{top} );
436         my $text = $ConfigMenu{$m}{text};
437         if ( $m eq $menu ) {
438             $groupText .= <<EOF;
439 <td bgcolor="grey"><a href="javascript:menuSubmit('$m')"><b>$text</b></a></td>
440 EOF
441         } else {
442             $groupText .= <<EOF;
443 <td><a href="javascript:menuSubmit('$m')">$text</a></td>
444 EOF
445         }
446     }
447
448     if ( $host eq "" ) {
449         $content .= <<EOF;
450 ${h1("Main Configuration Editor")}
451 EOF
452     } else {
453         $content .= <<EOF;
454 ${h1("Host $host Configuration Editor")}
455 <p>
456 Note: Check Override if you want to modify a value specific to this host.
457 EOF
458     }
459
460     my $saveDisplay = "block";
461     $saveDisplay = "none" if ( !$In{modified} );
462     $content .= <<EOF;
463 <table border="0" cellpadding="2">
464 <tr>$groupText</tr>
465 <tr>
466 <form method="post" name="form1" action="$MyURL">
467 <input type="hidden" name="host" value="$host">
468 <input type="hidden" name="menu" value="$menu">
469 <input type="hidden" name="newMenu" value="">
470 <input type="hidden" name="modified" value="$In{modified}">
471 <input type="hidden" name="deleteVar" value="">
472 <input type="hidden" name="insertVar" value="">
473 <input type="hidden" name="addVar" value="">
474 <input type="hidden" name="action" value="editConfig">
475 <input type="submit" style="display: $saveDisplay" name="editAction" value="Save">
476 $contentHidden
477
478 <script language="javascript" type="text/javascript">
479 <!--
480
481     function deleteSubmit(varName)
482     {
483         document.form1.deleteVar.value = varName;
484         document.form1.modified.value = 1;
485         document.form1.submit();
486         return;
487     }
488
489     function insertSubmit(varName)
490     {
491         document.form1.insertVar.value = varName;
492         document.form1.modified.value = 1;
493         document.form1.submit();
494         return;
495     }
496
497     function addSubmit(varName, checkKey)
498     {
499         if ( checkKey && document.form1.addVarKey.value == "" ) {
500             alert("New key must be non-empty");
501             return;
502         }
503         document.form1.addVar.value = varName;
504         document.form1.modified.value = 1;
505         document.form1.submit();
506         return;
507     }
508
509     function menuSubmit(menuName)
510     {
511         document.form1.newMenu.value = menuName;
512         document.form1.submit();
513     }
514
515     function varChange(varName)
516     {
517         document.form1.editAction.style.display = "block";
518         document.form1.modified.value = 1;
519     }
520
521     function checkboxChange(varName)
522     {
523         document.form1.editAction.style.display = "block";
524         document.form1.modified.value = 1;
525         // Do nothing if the checkbox is now set
526         if ( eval("document.form1.override_" + varName + ".checked") ) {
527             return false;
528         }
529         var allVars = {};
530         var varRE  = new RegExp("^v_(" + varName + ".*)");
531         var origRE = new RegExp("^orig_(" + varName + ".*)");
532         for ( var i = 0 ; i < document.form1.elements.length ; i++ ) {
533             var e = document.form1.elements[i];
534             var re;
535             if ( (re = varRE.exec(e.name)) != null ) {
536                 if ( allVars[re[1]] == null ) {
537                     allVars[re[1]] = 0;
538                 }
539                 allVars[re[1]]++;
540                 //debugMsg("found v_ match with " + re[1]);
541                 //debugMsg("allVars[" + re[1] + "] = " + allVars[re[1]]);
542             } else if ( (re = origRE.exec(e.name)) != null ) {
543                 if ( allVars[re[1]] == null ) {
544                     allVars[re[1]] = 0;
545                 }
546                 allVars[re[1]]--;
547                 //debugMsg("allVars[" + re[1] + "] = " + allVars[re[1]]);
548             }
549         }
550         var sameShape = 1;
551         for ( v in allVars ) {
552             if ( allVars[v] != 0 ) {
553                 //debugMsg("Not the same shape because of " + v);
554                 sameShape = 0;
555             }
556         }
557         if ( sameShape ) {
558             for ( v in allVars ) {
559                 //debugMsg("setting " + v);
560                 eval("document.form1.v_" + v + ".value = document.form1.orig_" + v + ".value");
561             }
562             return true;
563         } else {
564             document.form1.submit();
565             return false;
566         }
567     }
568
569     function checkboxSet(varName)
570     {
571         document.form1.editAction.style.display = "block";
572         document.form1.modified.value = 1;
573         eval("document.form1.override_" + varName + ".checked = 1;");
574         return false;
575     }
576
577     var debugCounter = 0;
578     function debugMsg(msg)
579     {
580         debugCounter++;
581         var t = document.createTextNode(debugCounter + ": " + msg);
582         var br = document.createElement("br");
583         var debug = document.getElementById("debug");
584         debug.appendChild(t);
585         debug.appendChild(br);
586     }
587
588     function displayHelp(varName)
589     {
590         var help = document.getElementById("id_" + varName);
591         help.style.display = help.style.display == "block" ? "none" : "block";
592     }
593
594 //-->
595 </script>
596
597 <span id="debug"></span>
598
599 EOF
600
601     $content .= <<EOF;
602 <table border="1" cellspacing="0">
603 EOF
604
605     my $doneParam = {};
606
607     #
608     # There is a special case of the user deleting just the field
609     # that has the error(s).  So if the delete variable is a match
610     # or parent to all the errors then ignore the errors.
611     #
612     if ( $In{deleteVar} ne "" && %$errors > 0 ) {
613         my $matchAll = 1;
614         foreach my $v ( keys(%$errors) ) {
615             if ( $v ne $In{deleteVar} && $v !~ /^\Q$In{deleteVar}_/ ) {
616                 $matchAll = 0;
617                 last;
618             }
619         }
620         $errors = {} if ( $matchAll );
621     }
622
623     my $isError = %$errors;
624
625     if ( !$isError && $In{editAction} eq "Save" ) {
626         my $mesg;
627         if ( $host ne "" ) {
628             $hostConf = $bpc->ConfigDataRead($host) if ( !defined($hostConf) );
629             $mesg = configDiffMesg($host, $hostConf, $newConf);
630             foreach my $param ( %$newConf ) {
631                 $hostConf->{$param} = $newConf->{$param}
632                                 if ( $override->{param} );
633             }
634             $bpc->ConfigDataWrite($host, $hostConf);
635         } else {
636             $mainConf = $bpc->ConfigDataRead() if ( !defined($mainConf) );
637             $mesg = configDiffMesg(undef, $mainConf, $newConf);
638             $mainConf = { %$mainConf, %$newConf };
639             $bpc->ConfigDataWrite(undef, $mainConf);
640         }
641         if ( $mesg ne "" ) {
642             $bpc->ServerConnect();
643             foreach my $str ( split(/\n/, $mesg) ) {
644                 $bpc->ServerMesg($str);
645             }
646         }
647     }
648
649     my @mask = @{$menuDisable{$menu}{mask} || []};
650
651     foreach my $paramInfo ( @{$ConfigMenu{$menu}{param}} ) {
652
653         my $param    = $paramInfo->{name};
654         my $disabled = shift(@mask);
655
656         next if ( $disabled || $menuDisable{$menu}{top} );
657         if ( ref($paramInfo->{visible}) eq "CODE"
658                         && !&{$paramInfo->{visible}}($newConf) ) {
659             next;
660         }
661
662         if ( defined(my $text = $paramInfo->{text}) ) {
663             $content .= <<EOF;
664 <tr><td colspan="2" class="editHeader">$text</td></tr>
665 EOF
666             next;
667         }
668
669         #
670         # TODO: get parameter documentation
671         #
672         my $comment = "";
673         $comment =~ s/\'//g;
674         $comment =~ s/\"//g;
675         $comment =~ s/\n/ /g;
676
677         $doneParam->{$param} = 1;
678
679         $content .= fieldEditBuild($ConfigMeta{$param},
680                                 $param,
681                                 $newConf->{$param},
682                                 $errors,
683                                 0,
684                                 $comment,
685                                 $isError,
686                                 $paramInfo->{onchangeSubmit},
687                                 defined($override) ? $param : undef,
688                                 defined($override) ? $override->{$param} : undef
689                         );
690     }
691
692     #
693     # Emit any remaining errors - should not happen
694     #
695     foreach my $param ( sort(keys(%$errors)) ) {
696         $content .= <<EOF;
697 <tr><td colspan="2" class="border">$errors->{$param}</td></tr>
698 EOF
699         delete($errors->{$param});
700     }
701
702     $content .= <<EOF;
703 </table>
704 EOF
705
706     #
707     # Emit all the remaining editable config settings as hidden values
708     #
709     foreach my $param ( keys(%ConfigMeta) ) {
710         next if ( $doneParam->{$param} );
711         next if ( $userHost && !$bpc->{Conf}{CgiUserConfigEdit}{$param} );
712         $content .= fieldHiddenBuild($ConfigMeta{$param},
713                             $param,
714                             $newConf->{$param},
715                             "v"
716                         );
717         if ( defined($override) ) {
718             $content .= <<EOF;
719 <input type="hidden" name="override_$param" value="$override->{$param}">
720 EOF
721         }
722         $doneParam->{$param} = 1;
723     }
724
725     $content .= <<EOF;
726 </form>
727 </tr>
728 </table>
729 EOF
730
731     Header("Config Edit", $content);
732     Trailer();
733 }
734
735 sub fieldHiddenBuild
736 {
737     my($type, $varName, $varValue, $prefix) = @_;
738     my $content;
739
740     $type = { type => $type } if ( ref($type) ne "HASH" );
741
742     if ( $type->{type} eq "list" ) {
743         $varValue = [] if ( !defined($varValue) );
744         $varValue = [$varValue] if ( ref($varValue) ne "ARRAY" );
745
746         for ( my $i = 0 ; $i < @$varValue ; $i++ ) {
747             $content .= fieldHiddenBuild($type->{child}, "${varName}_$i",
748                                          $varValue->[$i], $prefix);
749         }
750     } elsif ( $type->{type} eq "hash" ) {
751         $varValue = {} if ( ref($varValue) ne "HASH" );
752         my(@order, $childType);
753
754         if ( defined($type->{child}) ) {
755             @order = sort(keys(%{$type->{child}}));
756         } else {
757             @order = sort(keys(%$varValue));
758         }
759
760         foreach my $fld ( @order ) {
761             if ( defined($type->{child}) ) {
762                 $childType = $type->{child}{$fld};
763             } else {
764                 $childType = $type->{childType};
765                 #
766                 # emit list of fields since they are user-defined
767                 # rather than hard-coded
768                 #
769                 $content .= <<EOF;
770 <input type="hidden" name="vflds.$varName" value="${EscHTML($fld)}">
771 EOF
772             }
773             $content .= fieldHiddenBuild($childType, "${varName}_$fld",
774                                          $varValue->{$fld}, $prefix);
775         }
776     } elsif ( $type->{type} eq "shortlist" ) {
777         $varValue = [$varValue] if ( ref($varValue) ne "ARRAY" );
778         $varValue = join(", ", @$varValue);
779         $content .= <<EOF;
780 <input type="hidden" name="${prefix}_$varName" value="${EscHTML($varValue)}">
781 EOF
782     } else {
783         $content .= <<EOF;
784 <input type="hidden" name="${prefix}_$varName" value="${EscHTML($varValue)}">
785 EOF
786     }
787     return $content;
788 }
789
790 sub fieldEditBuild
791 {
792     my($type, $varName, $varValue, $errors, $level, $comment, $isError,
793        $onchangeSubmit, $overrideVar, $overrideSet) = @_;
794
795     my $content;
796     my $size = 50 - 10 * $level;
797     $type = { type => $type } if ( ref($type) ne "HASH" );
798
799     if ( $level == 0 ) {
800         $content .= <<EOF;
801 <tr id="id_$varName" class="optionalComment"><td colspan="2">$comment</td></tr>
802 <tr><td class="border"><a href="javascript: displayHelp('$varName')">$varName</a>
803 EOF
804         if ( defined($overrideVar) ) {
805             my $override_checked = "";
806             if ( !$isError && $In{deleteVar}       =~ /^\Q${varName}_/
807                     || !$isError && $In{insertVar} =~ /^\Q${varName}\E(_|$)/
808                     || !$isError && $In{addVar}    =~ /^\Q${varName}\E(_|$)/ ) {
809                 $overrideSet = 1;
810             }
811             if ( $overrideSet ) {
812                 $override_checked = "checked";
813             }
814             $content .= <<EOF;
815 <br><input type="checkbox" name="override_$varName" $override_checked value="1" onClick="checkboxChange('$varName')">\&nbsp;Override
816 EOF
817         }
818         $content .= "</td>\n";
819     }
820
821     $content .= "<td class=\"border\">\n";
822     if ( $type->{type} eq "list" ) {
823         $varValue = [] if ( !defined($varValue) );
824         $varValue = [$varValue] if ( ref($varValue) ne "ARRAY" );
825         if ( !$isError && $In{deleteVar} =~ /^\Q${varName}_\E(\d+)$/
826                 && $1 < @$varValue ) {
827             #
828             # User deleted entry in this array
829             #
830             splice(@$varValue, $1, 1) if ( @$varValue > 1 || $type->{emptyOk} );
831             $In{deleteVar} = "";
832         }
833         if ( !$isError && $In{insertVar} =~ /^\Q${varName}_\E(\d+)$/
834                 && $1 < @$varValue ) {
835             #
836             # User inserted entry in this array
837             #
838             splice(@$varValue, $1, 0, "")
839                         if ( @$varValue > 1 || $type->{emptyOk} );
840             $In{insertVar} = "";
841         }
842         if ( !$isError && $In{addVar} eq $varName ) {
843             #
844             # User added entry to this array
845             #
846             push(@$varValue, undef);
847             $In{addVar} = "";
848         }
849         $content .= "<table border=\"1\" cellspacing=\"0\">\n";
850
851         for ( my $i = 0 ; $i < @$varValue ; $i++ ) {
852             $content .= "<tr><td class=\"border\">\n";
853             if ( @$varValue > 1 || $type->{emptyOk} ) {
854                 $content .= <<EOF;
855 <input type="button" name="ins_${varName}_$i" value="Insert"
856     onClick="insertSubmit('${varName}_$i')">
857 <input type="button" name="del_${varName}_$i" value="Delete"
858     onClick="deleteSubmit('${varName}_$i')">
859 EOF
860             }
861             $content .= "</td>\n";
862             $content .= fieldEditBuild($type->{child}, "${varName}_$i",
863                                 $varValue->[$i], $errors, $level + 1, undef,
864                                 $isError, $onchangeSubmit,
865                                 $overrideVar, $overrideSet);
866             $content .= "</tr>\n";
867         }
868         $content .= <<EOF;
869 <tr><td class="border"><input type="button" name="add_$varName" value="Add"
870     onClick="addSubmit('$varName')"></td></tr>
871 </table>
872 EOF
873     } elsif ( $type->{type} eq "hash" ) {
874         $content .= "<table border=\"1\" cellspacing=\"0\">\n";
875         $varValue = {} if ( ref($varValue) ne "HASH" );
876
877         if ( !$isError && !$type->{noKeyEdit}
878                         && $In{deleteVar} =~ /^\Q${varName}_\E(\w+)$/ ) {
879             #
880             # User deleted entry in this array
881             #
882             delete($varValue->{$1}) if ( keys(%$varValue) > 1
883                                             || $type->{emptyOk} );
884             $In{deleteVar} = "";
885         }
886         if ( !$isError && !defined($type->{child})
887                         && $In{addVar} eq $varName ) {
888             #
889             # User added entry to this array
890             #
891             $varValue->{$In{addVarKey}} = ""
892                             if ( !defined($varValue->{$In{addVarKey}}) );
893             $In{addVar} = "";
894         }
895         my(@order, $childType);
896
897         if ( defined($type->{child}) ) {
898             @order = sort(keys(%{$type->{child}}));
899         } else {
900             @order = sort(keys(%$varValue));
901         }
902
903         foreach my $fld ( @order ) {
904             $content .= <<EOF;
905 <tr><td class="border">$fld
906 EOF
907             if ( !$type->{noKeyEdit}
908                     && (keys(%$varValue) > 1 || $type->{emptyOk}) ) {
909                 $content .= <<EOF;
910 <input type="submit" name="del_${varName}_$fld" value="Delete"
911         onClick="deleteSubmit('${varName}_$fld')">
912 EOF
913             }
914             if ( defined($type->{child}) ) {
915                 $childType = $type->{child}{$fld};
916             } else {
917                 $childType = $type->{childType};
918                 #
919                 # emit list of fields since they are user-defined
920                 # rather than hard-coded
921                 #
922                 $content .= <<EOF;
923 <input type="hidden" name="vflds.$varName" value="${EscHTML($fld)}">
924 EOF
925             }
926             $content .= "</td>\n";
927             $content .= fieldEditBuild($childType, "${varName}_$fld",
928                             $varValue->{$fld}, $errors, $level + 1, undef,
929                             $isError, $onchangeSubmit,
930                             $overrideVar, $overrideSet);
931             $content .= "</tr>\n";
932         }
933
934         if ( !$type->{noKeyEdit} ) {
935             $content .= <<EOF;
936 <tr><td class="border" colspan="2">
937 New key: <input type="text" name="addVarKey" size="20" maxlength="256" value="">
938 <input type="button" name="add_$varName" value="Add" onClick="addSubmit('$varName', 1)">
939 </td></tr>
940 EOF
941         }
942         $content .= "</table>\n";
943     } else {
944         if ( $isError ) {
945             #
946             # If there was an error, we use the original post values
947             # in %In, rather than the parsed values in $varValue.
948             # This is so that the user's erroneous input is preserved.
949             #
950             $varValue = $In{"v_$varName"} if ( defined($In{"v_$varName"}) );
951         }
952         if ( defined($errors->{$varName}) ) {
953             $content .= <<EOF;
954 $errors->{$varName}<br>
955 EOF
956             delete($errors->{$varName});
957         }
958         my $onChange;
959         if ( defined($overrideVar) ) {
960             $onChange .= "checkboxSet('$overrideVar');";
961         } else {
962             $onChange .= "varChange('$overrideVar');";
963         }
964         if ( $onchangeSubmit ) {
965             $onChange .= "document.form1.submit();";
966         }
967         if ( $onChange ne "" ) {
968             $onChange = " onChange=\"$onChange\"";
969         }
970         if ( $varValue !~ /\n/ &&
971                 ($type->{type} eq "integer"
972                     || $type->{type} eq "string"
973                     || $type->{type} eq "shortlist"
974                     || $type->{type} eq "float") ) {
975             # simple input box
976             if ( $type->{type} eq "shortlist" ) {
977                 $varValue = [$varValue] if ( ref($varValue) ne "ARRAY" );
978                 $varValue = join(", ", @$varValue);
979             }
980             $content .= <<EOF;
981 <input type="text" name="v_$varName" size="$size" maxlength="256" value="${EscHTML($varValue)}"$onChange>
982 EOF
983         } elsif ( $type->{type} eq "boolean" ) {
984             # checkbox
985             my $checked = "checked" if ( $varValue );
986             $content .= <<EOF;
987 <input type="checkbox" name="v_$varName" $checked value="1">
988 EOF
989         } elsif ( $type->{type} eq "select" ) {
990             $content .= <<EOF;
991 <select name="v_$varName"$onChange>
992 EOF
993             foreach my $option ( @{$type->{values}} ) {
994                 my $sel = " selected" if ( $varValue eq $option );
995                 $content .= "<option$sel>$option</option>\n";
996             }
997             $content .= "</select>\n";
998         } else {
999             # multi-line text area - count number of lines
1000             my $rowCnt = $varValue =~ tr/\n//;
1001             $rowCnt = 1 if ( $rowCnt < 1 );
1002             $content .= <<EOF;
1003 <textarea name="v_$varName" cols="$size" rows="$rowCnt"$onChange>${EscHTML($varValue)}</textarea>
1004 EOF
1005         }
1006     }
1007     $content .= "</td>\n";
1008     return $content;
1009 }
1010
1011 sub errorCheck
1012 {
1013     my $errors = {};
1014
1015     foreach my $param ( keys(%ConfigMeta) ) {
1016         fieldErrorCheck($ConfigMeta{$param}, $param, $errors);
1017     }
1018     return $errors;
1019 }
1020
1021 sub fieldErrorCheck
1022 {
1023     my($type, $varName, $errors) = @_;
1024
1025     $type = { type => $type } if ( ref($type) ne "HASH" );
1026
1027     if ( $type->{type} eq "list" ) {
1028         for ( my $i = 0 ; ; $i++ ) {
1029             last if ( fieldErrorCheck($type->{child}, "${varName}_$i", $errors) );
1030         }
1031     } elsif ( $type->{type} eq "hash" ) {
1032         my(@order, $childType);
1033         my $ret;
1034
1035         if ( defined($type->{child}) ) {
1036             @order = sort(keys(%{$type->{child}}));
1037         } else {
1038             @order = split(/\0/, $In{"vflds.$varName"});
1039         }
1040         foreach my $fld ( @order ) {
1041             if ( defined($type->{child}) ) {
1042                 $childType = $type->{child}{$fld};
1043             } else {
1044                 $childType = $type->{childType};
1045             }
1046             $ret ||= fieldErrorCheck($childType, "${varName}_$fld", $errors);
1047         }
1048         return $ret;
1049     } else {
1050         return 1 if ( !exists($In{"v_$varName"}) );
1051
1052         if ( $type->{type} eq "integer"
1053                 || $type->{type} eq "boolean" ) {
1054             if ( $In{"v_$varName"} !~ /^-?\d+\s*$/s
1055                             && $In{"v_$varName"} ne "" ) {
1056                 $errors->{$varName} = "Error: $varName must be an integer";
1057             }
1058         } elsif ( $type->{type} eq "float" ) {
1059             if ( $In{"v_$varName"} !~ /^-?\d*(\.\d*)?\s*$/s
1060                             && $In{"v_$varName"} ne "" ) {
1061                 $errors->{$varName}
1062                         = "Error: $varName must be a real-valued number";
1063             }
1064         } elsif ( $type->{type} eq "shortlist" ) {
1065             my @vals = split(/[,\s]+/, $In{"v_$varName"});
1066             for ( my $i = 0 ; $i < @vals ; $i++ ) {
1067                 if ( $type->{child} eq "integer"
1068                         && $vals[$i] !~ /^-?\d+\s*$/s
1069                         && $vals[$i] ne "" ) {
1070                     my $k = $i + 1;
1071                     $errors->{$varName} = "Error: $varName entry $k must"
1072                                         . " be an integer";
1073                 } elsif ( $type->{child} eq "float"
1074                         && $vals[$i] !~ /^-?\d*(\.\d*)?\s*$/s
1075                         && $vals[$i] ne "" ) {
1076                     my $k = $i + 1;
1077                     $errors->{$varName} = "Error: $varName entry $k must"
1078                                         . " be a real-valued number";
1079                 }
1080             }
1081         } elsif ( $type->{type} eq "select" ) {
1082             my $match = 0;
1083             foreach my $option ( @{$type->{values}} ) {
1084                 if ( $In{"v_$varName"} eq $option ) {
1085                     $match = 1;
1086                     last;
1087                 }
1088             }
1089             $errors->{$varName} = "Error: $varName must be a valid option"
1090                             if ( !$match );
1091         } else {
1092             #
1093             # $type->{type} eq "string": no error checking
1094             #
1095         }
1096     }
1097     return 0;
1098 }
1099
1100 sub inputParse
1101 {
1102     my($bpc, $userHost) = @_;
1103     my $conf     = {};
1104     my $override = {};
1105
1106     foreach my $param ( keys(%ConfigMeta) ) {
1107         my $value;
1108         next if ( $userHost && !$bpc->{Conf}{CgiUserConfigEdit}{$param} );
1109         fieldInputParse($ConfigMeta{$param}, $param, \$value);
1110         $conf->{$param}     = $value;
1111         $override->{$param} = $In{"override_$param"};
1112 }
1113     return ($conf, $override);
1114 }
1115
1116 sub fieldInputParse
1117 {
1118     my($type, $varName, $value) = @_;
1119
1120     $type = { type => $type } if ( ref($type) ne "HASH" );
1121
1122     if ( $type->{type} eq "list" ) {
1123         $$value = [];
1124         for ( my $i = 0 ; ; $i++ ) {
1125             my $val;
1126             last if ( fieldInputParse($type->{child}, "${varName}_$i", \$val) );
1127             push(@$$value, $val);
1128         }
1129         $$value = undef if ( $type->{undefIfEmpty} && @$$value == 0 );
1130     } elsif ( $type->{type} eq "hash" ) {
1131         my(@order, $childType);
1132         my $ret;
1133         $$value = {};
1134
1135         if ( defined($type->{child}) ) {
1136             @order = sort(keys(%{$type->{child}}));
1137         } else {
1138             @order = split(/\0/, $In{"vflds.$varName"});
1139         }
1140
1141         foreach my $fld ( @order ) {
1142             my $val;
1143             if ( defined($type->{child}) ) {
1144                 $childType = $type->{child}{$fld};
1145             } else {
1146                 $childType = $type->{childType};
1147             }
1148             $ret ||= fieldInputParse($childType, "${varName}_$fld", \$val);
1149             last if ( $ret );
1150             $$value->{$fld} = $val;
1151         }
1152         return $ret;
1153     } else {
1154         if ( $type->{type} eq "boolean" ) {
1155             $$value = 0 + $In{"v_$varName"};
1156         } elsif ( !exists($In{"v_$varName"}) ) {
1157             return 1;
1158         }
1159
1160         if ( $type->{type} eq "integer" ) {
1161             $$value = 0 + $In{"v_$varName"};
1162         } elsif ( $type->{type} eq "float" ) {
1163             $$value = 0 + $In{"v_$varName"};
1164         } elsif ( $type->{type} eq "shortlist" ) {
1165             $$value = [split(/[,\s]+/, $In{"v_$varName"})];
1166             if ( $type->{child} eq "float"
1167                     || $type->{child} eq "integer"
1168                     || $type->{child} eq "boolean" ) {
1169                 foreach ( @$$value ) {
1170                     $_ += 0;
1171                 }
1172             }
1173         } else {
1174             $$value = $In{"v_$varName"};
1175         }
1176         $$value = undef if ( $type->{undefIfEmpty} && $$value eq "" );
1177     }
1178     return 0;
1179 }
1180
1181 sub configDiffMesg
1182 {
1183     my($host, $oldConf, $newConf) = @_;
1184     my $mesg;
1185     my $conf;
1186
1187     if ( $host ne "" ) {
1188         $conf = "host $host config";
1189     } else {
1190         $conf = "main config";
1191     }
1192
1193     foreach my $p ( keys(%ConfigMeta) ) {
1194         if ( !exists($oldConf->{$p}) && !exists($newConf->{$p}) ) {
1195             next;
1196         } elsif ( exists($oldConf->{$p}) && !exists($newConf->{$p}) ) {
1197             $mesg .= "log Deleted $p from $conf\n";
1198         } elsif ( !exists($oldConf->{$p}) && exists($newConf->{$p}) ) {
1199             my $dump = Data::Dumper->new([$newConf->{$p}]);
1200             $dump->Indent(0);
1201             $dump->Sortkeys(1);
1202             $dump->Terse(1);
1203             my $value = $dump->Dump;
1204             $mesg .= "log Added $p to $conf, set to $value\n";
1205         } else {
1206             my $dump = Data::Dumper->new([$newConf->{$p}]);
1207             $dump->Indent(0);
1208             $dump->Sortkeys(1);
1209             $dump->Terse(1);
1210             my $valueNew = $dump->Dump;
1211
1212             my $v = $oldConf->{$p};
1213             if ( ref($newConf->{$p}) eq "ARRAY" && ref($v) eq "" ) {
1214                 $v = [$v];
1215             }
1216             $dump = Data::Dumper->new([$v]);
1217             $dump->Indent(0);
1218             $dump->Sortkeys(1);
1219             $dump->Terse(1);
1220             my $valueOld = $dump->Dump;
1221
1222             $mesg .= "log Changed $p in $conf to $valueNew from $valueOld\n"
1223                                     if ( $valueOld ne $valueNew );
1224         }
1225     }
1226     return $mesg;
1227 }
1228
1229 1;