0be716acc315970f5288db3658a640b4695842fe
[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) 2005-2007  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 3.1.0beta0, released 3 Sep 2007.
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 use Encode;
45
46 our %ConfigMenu = (
47     server => {
48         text  => "CfgEdit_Title_Server",
49         param => [
50             {text => "CfgEdit_Title_General_Parameters"},
51             {name => "ServerHost"},
52             {name => "BackupPCUser"},
53             {name => "BackupPCUserVerify"},
54             {name => "MaxOldLogFiles"},
55             {name => "TrashCleanSleepSec"},
56
57             {text => "CfgEdit_Title_Wakeup_Schedule"},
58             {name => "WakeupSchedule"},
59
60             {text => "CfgEdit_Title_Concurrent_Jobs"},
61             {name => "MaxBackups"},
62             {name => "MaxUserBackups"},
63             {name => "MaxPendingCmds"},
64             {name => "MaxBackupPCNightlyJobs"},
65             {name => "BackupPCNightlyPeriod"},
66
67             {text => "CfgEdit_Title_Pool_Filesystem_Limits"},
68             {name => "DfCmd"},
69             {name => "DfMaxUsagePct"},
70             {name => "HardLinkMax"},
71
72             {text => "CfgEdit_Title_Other_Parameters"},
73             {name => "UmaskMode"},
74             {name => "MyPath"},
75             {name => "DHCPAddressRanges"},
76             {name => "PerlModuleLoad"},
77             {name => "ServerInitdPath"},
78             {name => "ServerInitdStartCmd"},
79
80             {text => "CfgEdit_Title_Remote_Apache_Settings"},
81             {name => "ServerPort"},
82             {name => "ServerMesgSecret"},
83
84             {text => "CfgEdit_Title_Program_Paths"},
85             {name => "SshPath"},
86             {name => "NmbLookupPath"},
87             {name => "PingPath"},
88             {name => "DfPath"},
89             {name => "SplitPath"},
90             {name => "ParPath"},
91             {name => "CatPath"},
92             {name => "GzipPath"},
93             {name => "Bzip2Path"},
94
95             {text => "CfgEdit_Title_Install_Paths"},
96             #
97             # Can only edit TopDir and LogDir if we are in FHS mode.
98             # Otherwise they are hardcoded in lib/BackupPC/Lib.pm.
99             #
100             {name => "TopDir",
101                     visible => sub { return $_[1]->useFHS(); } },
102             {name => "LogDir",
103                     visible => sub { return $_[1]->useFHS(); } },
104             {name => "CgiDir"},
105             #
106             # Cannot edit ConfDir or InstallDir, since the real value is hardcoded in
107             # lib/BackupPC/Lib.pm.
108             # {name => "ConfDir"},
109             # {name => "InstallDir"},
110             #
111         ],
112     },
113     email => {
114         text  => "CfgEdit_Title_Email",
115         param => [
116             {text => "CfgEdit_Title_Email_settings"},
117             {name => "SendmailPath"},
118             {name => "EMailNotifyMinDays"},
119             {name => "EMailFromUserName"},
120             {name => "EMailAdminUserName"},
121             {name => "EMailUserDestDomain"},
122
123             {text => "CfgEdit_Title_Email_User_Messages"},
124             {name => "EMailNoBackupEverSubj"},
125             {name => "EMailNoBackupEverMesg"},
126             {name => "EMailNotifyOldBackupDays"},
127             {name => "EMailNoBackupRecentSubj"},
128             {name => "EMailNoBackupRecentMesg"},
129             {name => "EMailNotifyOldOutlookDays"},
130             {name => "EMailOutlookBackupSubj"},
131             {name => "EMailOutlookBackupMesg"},
132             {name => "EMailHeaders"},
133         ],
134     },
135     cgi => {
136         text => "CfgEdit_Title_CGI",
137         param => [
138             {text => "CfgEdit_Title_Admin_Privileges"},
139             {name => "CgiAdminUserGroup"},
140             {name => "CgiAdminUsers"},
141
142             {text => "CfgEdit_Title_Page_Rendering"},
143             {name => "Language"},
144             {name => "CgiNavBarAdminAllHosts"},
145             {name => "CgiSearchBoxEnable"},
146             {name => "CgiNavBarLinks"},
147             {name => "CgiStatusHilightColor"},
148             {name => "CgiDateFormatMMDD"},
149             {name => "CgiHeaders"},
150             {name => "CgiExt2ContentType"},
151             {name => "CgiCSSFile"},
152
153             {text => "CfgEdit_Title_Paths"},
154             {name => "CgiURL"},
155             {name => "CgiImageDir"},
156             {name => "CgiImageDirURL"},
157
158             {text => "CfgEdit_Title_User_URLs"},
159             {name => "CgiUserHomePageCheck"},
160             {name => "CgiUserUrlCreate"},
161
162             {text => "CfgEdit_Title_User_Config_Editing"},
163             {name => "CgiUserConfigEditEnable"},
164             {name => "CgiUserConfigEdit"},
165         ],
166     },
167     xfer => {
168         text => "CfgEdit_Title_Xfer",
169         param => [
170             {text => "CfgEdit_Title_Xfer_Settings"},
171             {name => "XferMethod", onchangeSubmit => 1},
172             {name => "XferLogLevel"},
173             {name => "ClientCharset"},
174             {name => "ClientCharsetLegacy"},
175
176             {text => "CfgEdit_Title_Smb_Settings",
177                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
178             {name => "SmbShareName",
179                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
180             {name => "SmbShareUserName",
181                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
182             {name => "SmbSharePasswd",
183                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
184
185             {text => "CfgEdit_Title_Tar_Settings",
186                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
187             {name => "TarShareName",
188                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
189
190             {text => "CfgEdit_Title_Rsync_Settings",
191                 visible => sub { return $_[0]->{XferMethod} eq "rsync"; } },
192             {text => "CfgEdit_Title_Rsyncd_Settings",
193                 visible => sub { return $_[0]->{XferMethod} eq "rsyncd"; } },
194             {name => "RsyncShareName",
195                 visible => sub { return $_[0]->{XferMethod} =~ /rsync/; } },
196             {name => "RsyncdUserName",
197                 visible => sub { return $_[0]->{XferMethod} eq "rsyncd"; } },
198             {name => "RsyncdPasswd",
199                 visible => sub { return $_[0]->{XferMethod} eq "rsyncd"; } },
200             {name => "RsyncdAuthRequired",
201                 visible => sub { return $_[0]->{XferMethod} eq "rsyncd"; } },
202             {name => "RsyncCsumCacheVerifyProb",
203                 visible => sub { return $_[0]->{XferMethod} =~ /rsync/; } },
204
205             {text => "CfgEdit_Title_BackupPCd_Settings",
206                 visible => sub { return $_[0]->{XferMethod} eq "backuppcd"; } },
207             {name => "BackupPCdShareName",
208                 visible => sub { return $_[0]->{XferMethod} eq "backuppcd"; } },
209             {name => "BackupPCdPath",
210                 visible => sub { return $_[0]->{XferMethod} eq "backuppcd"; } },
211             {name => "BackupPCdCmd",
212                 visible => sub { return $_[0]->{XferMethod} eq "backuppcd"; } },
213             {name => "BackupPCdRestoreCmd",
214                 visible => sub { return $_[0]->{XferMethod} eq "backuppcd"; } },
215
216             {text => "CfgEdit_Title_Archive_Settings",
217                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
218             {name => "ArchiveDest",
219                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
220             {name => "ArchiveComp",
221                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
222             {name => "ArchivePar",
223                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
224             {name => "ArchiveSplit",
225                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
226
227             {text => "CfgEdit_Title_Include_Exclude",
228                 visible => sub { return $_[0]->{XferMethod} ne "archive"; } },
229             {name => "BackupFilesOnly",
230                 visible => sub { return $_[0]->{XferMethod} ne "archive"; } },
231             {name => "BackupFilesExclude",
232                 visible => sub { return $_[0]->{XferMethod} ne "archive"; } },
233
234             {text => "CfgEdit_Title_Smb_Paths_Commands",
235                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
236             {name => "SmbClientPath",
237                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
238             {name => "SmbClientFullCmd",
239                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
240             {name => "SmbClientIncrCmd",
241                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
242             {name => "SmbClientRestoreCmd",
243                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
244
245             {text => "CfgEdit_Title_Tar_Paths_Commands",
246                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
247             {name => "TarClientPath",
248                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
249             {name => "TarClientCmd",
250                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
251             {name => "TarFullArgs",
252                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
253             {name => "TarIncrArgs",
254                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
255             {name => "TarClientRestoreCmd",
256                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
257
258             {text => "CfgEdit_Title_Rsync_Paths_Commands_Args",
259                 visible => sub { return $_[0]->{XferMethod} eq "rsync"; } },
260             {text => "CfgEdit_Title_Rsyncd_Port_Args",
261                 visible => sub { return $_[0]->{XferMethod} eq "rsyncd"; } },
262             {name => "RsyncClientPath",
263                 visible => sub { return $_[0]->{XferMethod} eq "rsync"; } },
264             {name => "RsyncClientCmd",
265                 visible => sub { return $_[0]->{XferMethod} eq "rsync"; } },
266             {name => "RsyncClientRestoreCmd",
267                 visible => sub { return $_[0]->{XferMethod} eq "rsync"; } },
268             {name => "RsyncdClientPort",
269                 visible => sub { return $_[0]->{XferMethod} eq "rsyncd"; } },
270             {name => "RsyncArgs",
271                 visible => sub { return $_[0]->{XferMethod} =~ /rsync/; } },
272             {name => "RsyncRestoreArgs",
273                 visible => sub { return $_[0]->{XferMethod} =~ /rsync/; } },
274
275             {text => "CfgEdit_Title_Archive_Paths_Commands",
276                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
277             {name => "ArchiveClientCmd",
278                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
279
280         ],
281     },
282     schedule => {
283         text => "CfgEdit_Title_Schedule",
284         param => [
285             {text => "CfgEdit_Title_Full_Backups"},
286             {name => "FullPeriod"},
287             {name => "FullKeepCnt"},
288             {name => "FullKeepCntMin"},
289             {name => "FullAgeMax"},
290
291             {text => "CfgEdit_Title_Incremental_Backups"},
292             {name => "IncrPeriod"},
293             {name => "IncrKeepCnt"},
294             {name => "IncrKeepCntMin"},
295             {name => "IncrAgeMax"},
296             {name => "IncrLevels"},
297             {name => "IncrFill"},
298
299             {text => "CfgEdit_Title_Blackouts"},
300             {name => "BackupsDisable"},
301             {name => "BlackoutBadPingLimit"},
302             {name => "BlackoutGoodCnt"},
303             {name => "BlackoutPeriods"},
304
305             {text => "CfgEdit_Title_Other"},
306             {name => "PartialAgeMax"},
307             {name => "RestoreInfoKeepCnt"},
308             {name => "ArchiveInfoKeepCnt"},
309             {name => "BackupZeroFilesIsFatal"},
310         ],
311     },
312     backup => {
313         text => "CfgEdit_Title_Backup_Settings",
314         param => [
315             {text => "CfgEdit_Title_Client_Lookup"},
316             {name => "ClientNameAlias"},
317             {name => "NmbLookupCmd"},
318             {name => "NmbLookupFindHostCmd"},
319             {name => "FixedIPNetBiosNameCheck"},
320             {name => "PingCmd"},
321             {name => "PingMaxMsec"},
322             
323             {text => "CfgEdit_Title_Other"},
324             {name => "ClientTimeout"},
325             {name => "MaxOldPerPCLogFiles"},
326             {name => "CompressLevel"},
327
328             {text => "CfgEdit_Title_User_Commands"},
329             {name => "DumpPreUserCmd"},
330             {name => "DumpPostUserCmd"},
331             {name => "DumpPreShareCmd"},
332             {name => "DumpPostShareCmd"},
333             {name => "RestorePreUserCmd"},
334             {name => "RestorePostUserCmd"},
335             {name => "ArchivePreUserCmd"},
336             {name => "ArchivePostUserCmd"},
337             {name => "UserCmdCheckStatus"},
338         ],
339     },
340     hosts => {
341         text => "CfgEdit_Title_Hosts",
342         param => [
343             {text    => "CfgEdit_Title_Hosts"},
344             {name    => "Hosts",
345              comment => "CfgEdit_Hosts_Comment"},
346         ],
347     },
348 );
349
350 sub action
351 {
352     my $pc_dir = "$TopDir/pc";
353     my($content, $contentHidden, $newConf, $override, $mainConf, $hostConf);
354     my $errors = {};
355
356     my $host = $In{host};
357     my $menu = $In{menu} || "server";
358     my $hosts_path = $Hosts;
359     my $config_path = $host eq "" ? "$TopDir/conf/config.pl"
360                                   : "$TopDir/pc/$host/config.pl";
361
362     my $Privileged = CheckPermission($host)
363                        && ($PrivAdmin || $Conf{CgiUserConfigEditEnable});
364     my $userHost = 1 if ( defined($host) );
365     my $debugText;
366
367     if ( !$Privileged ) {
368         ErrorExit(eval("qq{$Lang->{Only_privileged_users_can_edit_config_files}}"));
369     }
370
371     if ( defined($In{menu}) || $In{saveAction} eq "Save" ) {
372         $errors = errorCheck();
373         if ( %$errors ) {
374             #
375             # If there are errors, then go back to the same menu
376             #
377             $In{saveAction} = "";
378             #$In{newMenu} = "";
379         }
380         if ( (my $var = $In{overrideUncheck}) ne "" ) {
381             #
382             # a compound variable was unchecked; delete extra
383             # variables to make the shape the same.
384             #
385             #print STDERR Dumper(\%In);
386             foreach my $v ( keys(%In) ) {
387                 next if ( $v !~ /^v_zZ_(\Q$var\E(_zZ_.*|$))/ );
388                 delete($In{$v}) if ( !defined($In{"orig_zZ_$1"}) );
389             }
390             delete($In{"vflds.$var"});
391         }
392
393         ($newConf, $override) = inputParse($bpc, $userHost);
394         $override = undef if ( $host eq "" );
395
396     } else {
397         #
398         # First time: pick up the current config settings
399         #
400         $mainConf = $bpc->ConfigDataRead();
401         if ( $host ne "" ) {
402             $hostConf = $bpc->ConfigDataRead($host);
403             $override = {};
404             foreach my $param ( keys(%$hostConf) ) {
405                 $override->{$param} = 1;
406             }
407         } else {
408             my $hostInfo = $bpc->HostInfoRead();
409             $hostConf = {};
410             $mainConf->{Hosts} = [map($hostInfo->{$_}, sort(keys(%$hostInfo)))];
411         }
412         $newConf = { %$mainConf, %$hostConf };
413     }
414
415     if ( $In{saveAction} ne "Save" && $In{newMenu} ne ""
416                     && defined($ConfigMenu{$In{newMenu}}) ) {
417         $menu = $In{newMenu};
418     }
419
420     my %menuDisable;
421     if ( $userHost ) {
422         #
423         # For a non-admin user editing the host config, we need to
424         # figure out which subsets of the menu tree will be visible,
425         # based on what is enabled.  Admin users can edit all the
426         # available per-host settings.
427         #
428         foreach my $m ( keys(%ConfigMenu) ) {
429             my $enabled = 0;
430             my $text = -1;
431             my $n = 0;
432             my @mask = ();
433
434             foreach my $paramInfo ( @{$ConfigMenu{$m}{param}} ) {
435                 my $param = $paramInfo->{name};
436                 if ( defined($paramInfo->{text}) ) {
437                     $text = $n;
438                     $mask[$text] = 1;
439                 } else {
440                     if ( $bpc->{Conf}{CgiUserConfigEdit}{$param}
441                           || (defined($bpc->{Conf}{CgiUserConfigEdit}{$param})
442                                 && $PrivAdmin) ) {
443                         $mask[$text] = 0 if ( $text >= 0 );
444                         $mask[$n] = 0;
445                         $enabled ||= 1;
446                     } else {
447                         $mask[$n] = 1;
448                     }
449                 }
450                 $n++;
451             }
452             $menuDisable{$m}{mask} = \@mask;
453             $menuDisable{$m}{top}  = !$enabled;
454         }
455         if ( $menuDisable{$menu}{top} ) {
456             #
457             # Find an enabled menu if the current menu is empty
458             #
459             foreach my $m ( sort(keys(%menuDisable)) ) {
460                 if ( !$menuDisable{$m}{top} ) {
461                     $menu = $m;
462                     last;
463                 }
464             }
465         }
466     }
467
468     my $groupText;
469     foreach my $m ( keys(%ConfigMenu) ) {
470         next if ( $menuDisable{$m}{top} );
471         my $text = eval("qq($Lang->{$ConfigMenu{$m}{text}})");
472         if ( $m eq $menu ) {
473             $groupText .= <<EOF;
474 <td class="editTabSel"><a href="javascript:menuSubmit('$m')"><b>$text</b></a></td>
475 EOF
476         } else {
477             $groupText .= <<EOF;
478 <td class="editTabNoSel"><a href="javascript:menuSubmit('$m')">$text</a></td>
479 EOF
480         }
481     }
482
483     if ( $host eq "" ) {
484         $content .= eval("qq($Lang->{CfgEdit_Header_Main})");
485     } else {
486         $content .= eval("qq($Lang->{CfgEdit_Header_Host})");
487     }
488
489     my $saveStyle = "";
490     my $saveColor = "#ff0000";
491     
492     if ( $In{modified} && $In{saveAction} ne "Save" && !%$errors ) {
493         $saveStyle = "style=\"color:$saveColor\"";
494     } else {
495         $In{modified} = 0;
496     }
497
498     #
499     # Add action and host to the URL so the nav bar link is
500     # highlighted
501     #
502     my $url = "$MyURL?action=editConfig";
503     $url .= "&host=$host" if ( $host ne "" );
504     $content .= <<EOF;
505 <table border="0" cellpadding="2">
506 <tr>$groupText</tr>
507 <tr>
508 <form method="post" name="editForm" action="$url">
509 <input type="hidden" name="host" value="$host">
510 <input type="hidden" name="menu" value="$menu">
511 <input type="hidden" name="newMenu" value="">
512 <input type="hidden" name="modified" value="$In{modified}">
513 <input type="hidden" name="deleteVar" value="">
514 <input type="hidden" name="insertVar" value="">
515 <input type="hidden" name="overrideUncheck" value="">
516 <input type="hidden" name="addVar" value="">
517 <input type="hidden" name="action" value="editConfig">
518 <input type="hidden" name="saveAction" value="">
519 <input type="button" class="editSaveButton" name="editAction"
520     value="${EscHTML($Lang->{CfgEdit_Button_Save})}" $saveStyle
521     onClick="saveSubmit();">
522 <p>
523
524 <script language="javascript" type="text/javascript">
525 <!--
526
527     function saveSubmit()
528     {
529         if ( document.editForm.modified.value != 0 ) {
530             document.editForm.saveAction.value = 'Save';
531             document.editForm.submit();
532         }
533         return false;
534     }
535
536     function deleteSubmit(varName)
537     {
538         document.editForm.deleteVar.value = varName;
539         document.editForm.modified.value = 1;
540         document.editForm.submit();
541         return;
542     }
543
544     function insertSubmit(varName)
545     {
546         document.editForm.insertVar.value = varName;
547         document.editForm.modified.value = 1;
548         document.editForm.submit();
549         return;
550     }
551
552     function addSubmit(varName, checkKey)
553     {
554         if ( checkKey
555             && eval("document.editForm.addVarKey_" + varName + ".value") == "" ) {
556             alert("New key must be non-empty");
557             return;
558         }
559         document.editForm.addVar.value = varName;
560         document.editForm.modified.value = 1;
561         document.editForm.submit();
562         return;
563     }
564
565     function menuSubmit(menuName)
566     {
567         document.editForm.newMenu.value = menuName;
568         document.editForm.submit();
569     }
570
571     function varChange(varName)
572     {
573         document.editForm.modified.value = 1;
574         document.editForm.editAction.style.color = '$saveColor';
575     }
576
577     function checkboxChange(varName)
578     {
579         document.editForm.modified.value = 1;
580         document.editForm.editAction.style.color = '$saveColor';
581         // Do nothing if the checkbox is now set
582         if ( eval("document.editForm.override_" + varName + ".checked") ) {
583             return false;
584         }
585         var allVars = {};
586         var varRE  = new RegExp("^v_zZ_(" + varName + ".*)");
587         var origRE = new RegExp("^orig_zZ_(" + varName + ".*)");
588         for ( var i = 0 ; i < document.editForm.elements.length ; i++ ) {
589             var e = document.editForm.elements[i];
590             var re;
591             if ( (re = varRE.exec(e.name)) != null ) {
592                 if ( allVars[re[1]] == null ) {
593                     allVars[re[1]] = 0;
594                 }
595                 allVars[re[1]]++;
596                 //debugMsg("found v_zZ_ match with " + re[1]);
597                 //debugMsg("allVars[" + re[1] + "] = " + allVars[re[1]]);
598             } else if ( (re = origRE.exec(e.name)) != null ) {
599                 if ( allVars[re[1]] == null ) {
600                     allVars[re[1]] = 0;
601                 }
602                 allVars[re[1]]--;
603                 //debugMsg("allVars[" + re[1] + "] = " + allVars[re[1]]);
604             }
605         }
606         var sameShape = 1;
607         for ( v in allVars ) {
608             if ( allVars[v] != 0 ) {
609                 //debugMsg("Not the same shape because of " + v);
610                 sameShape = 0;
611             } else {
612                 // copy the original variable values
613                 //debugMsg("setting " + v);
614                 eval("document.editForm.v_zZ_" + v + ".value = document.editForm.orig_zZ_" + v + ".value");
615             }
616         }
617         if ( sameShape ) {
618             return true;
619         } else {
620             // need to rebuild the form since the compound variable
621             // has changed shape
622             document.editForm.overrideUncheck.value = varName;
623             document.editForm.submit();
624             return false;
625         }
626     }
627
628     function checkboxSet(varName)
629     {
630         document.editForm.modified.value = 1;
631         document.editForm.editAction.style.color = '$saveColor';
632         eval("document.editForm.override_" + varName + ".checked = 1;");
633         return false;
634     }
635
636     var debugCounter = 0;
637     function debugMsg(msg)
638     {
639         debugCounter++;
640         var t = document.createTextNode(debugCounter + ": " + msg);
641         var br = document.createElement("br");
642         var debug = document.getElementById("debug");
643         debug.appendChild(t);
644         debug.appendChild(br);
645     }
646
647     function displayHelp(varName)
648     {
649         var help = document.getElementById("id_" + varName);
650         help.style.display = help.style.display == "block" ? "none" : "block";
651     }
652
653 //-->
654 </script>
655
656 <span id="debug">$debugText</span>
657
658 EOF
659
660     $content .= <<EOF;
661 <table border="1" cellspacing="0">
662 EOF
663
664     my $doneParam = {};
665     my $tblContent;
666
667     #
668     # There is a special case of the user deleting just the field
669     # that has the error(s).  So if the delete variable is a match
670     # or parent to all the errors then ignore the errors.
671     #
672     if ( $In{deleteVar} ne "" && %$errors > 0 ) {
673         my $matchAll = 1;
674         foreach my $v ( keys(%$errors) ) {
675             if ( $v ne $In{deleteVar} && $v !~ /^\Q$In{deleteVar}_zZ_/ ) {
676                 $matchAll = 0;
677                 last;
678             }
679         }
680         $errors = {} if ( $matchAll );
681     }
682
683     my $isError = %$errors;
684
685     if ( !$isError && $In{saveAction} eq "Save" ) {
686         my($mesg, $err);
687         if ( $host ne "" ) {
688             $hostConf = $bpc->ConfigDataRead($host) if ( !defined($hostConf) );
689             my %hostConf2 = %$hostConf;
690             foreach my $param ( keys(%$newConf) ) {
691                 if ( $override->{$param} ) {
692                     $hostConf->{$param} = $newConf->{$param}
693                 } else {
694                     delete($hostConf->{$param});
695                 }
696             }
697             $mesg = configDiffMesg($host, \%hostConf2, $hostConf);
698             $err .= $bpc->ConfigDataWrite($host, $hostConf);
699         } else {
700             $mainConf = $bpc->ConfigDataRead() if ( !defined($mainConf) );
701
702             my $hostsSave = [];
703             my($hostsNew, $allHosts, $copyConf);
704             foreach my $entry ( @{$newConf->{Hosts}} ) {
705                 next if ( $entry->{host} eq "" );
706                 $allHosts->{$entry->{host}} = 1;
707                 $allHosts->{$1} = 1 if ( $entry->{host} =~ /(.+?)\s*=/ );
708             }
709             foreach my $entry ( @{$newConf->{Hosts}} ) {
710                 next if ( $entry->{host} eq ""
711                            || defined($hostsNew->{$entry->{host}}) );
712                 if ( $entry->{host} =~ /(.+?)\s*=\s*(.+)/ ) {
713                     if ( defined($allHosts->{$2}) ) {
714                         $entry->{host} = $1;
715                         $copyConf->{$1} = $2;
716                     } else {
717                         my $fullHost = $entry->{host};
718                         my $copyHost = $2;
719                         $err .= eval("qq($Lang->{CfgEdit_Error_Copy_host_does_not_exist})");
720                     }
721                 }
722                 push(@$hostsSave, $entry);
723                 $hostsNew->{$entry->{host}} = $entry;
724             }
725             ($mesg, my $hostChange) = hostsDiffMesg($hostsNew);
726             $bpc->HostInfoWrite($hostsNew) if ( $hostChange );
727             foreach my $host ( keys(%$copyConf) ) {
728                 #
729                 # Currently host names are forced to lc when they
730                 # are read from the hosts file.  Therefore we need
731                 # to force the from and to hosts to lc.
732                 #
733                 my $confData = $bpc->ConfigDataRead(lc($copyConf->{$host}));
734                 my $fromHost = $copyConf->{$host};
735                 $err  .= $bpc->ConfigDataWrite(lc($host), $confData);
736                 $mesg .= eval("qq($Lang->{CfgEdit_Log_Copy_host_config})");
737             }
738
739             delete($newConf->{Hosts});
740             $mesg .= configDiffMesg(undef, $mainConf, $newConf);
741             $mainConf = { %$mainConf, %$newConf };
742             $err .= $bpc->ConfigDataWrite(undef, $mainConf);
743             $newConf->{Hosts} = $hostsSave;
744         }
745         if ( defined($err) ) {
746             $tblContent .= <<EOF;
747 <tr><td colspan="2" class="border"><span class="editError">$err</span></td></tr>
748 EOF
749         }
750         $bpc->ServerConnect();
751         if ( $mesg ne "" ) {
752             (my $mesgBR = $mesg) =~ s/\n/<br>\n/g;
753              # uncomment this if you want the changes to be displayed
754 #            $tblContent .= <<EOF;
755 #<tr><td colspan="2" class="border"><span class="editComment">$mesgBR</span></td></tr>
756 #EOF
757             foreach my $str ( split(/\n/, $mesg) ) {
758                 $bpc->ServerMesg("log $str") if ( $str ne "" );
759             }
760         }
761         #
762         # Tell the server to reload, unless we only changed
763         # a client config
764         #
765         $bpc->ServerMesg("server reload") if ( $host eq "" );
766     }
767
768     my @mask = @{$menuDisable{$menu}{mask} || []};
769
770     foreach my $paramInfo ( @{$ConfigMenu{$menu}{param}} ) {
771
772         my $param    = $paramInfo->{name};
773         my $disabled = shift(@mask);
774
775         next if ( $disabled || $menuDisable{$menu}{top} );
776         if ( ref($paramInfo->{visible}) eq "CODE"
777                         && !&{$paramInfo->{visible}}($newConf, $bpc) ) {
778             next;
779         }
780
781         if ( defined($paramInfo->{text}) ) {
782             my $text = eval("qq($Lang->{$paramInfo->{text}})");
783             $tblContent .= <<EOF;
784 <tr><td colspan="2" class="editHeader">$text</td></tr>
785 EOF
786             next;
787         }
788
789         #
790         # TODO: get parameter documentation
791         #
792         my $comment = "";
793         #$comment =~ s/\'//g;
794         #$comment =~ s/\"//g;
795         #$comment =~ s/\n/ /g;
796
797         $doneParam->{$param} = 1;
798
799         $tblContent .= fieldEditBuild($ConfigMeta{$param},
800                               $param,
801                               $newConf->{$param},
802                               $errors,
803                               0,
804                               $comment,
805                               $isError,
806                               $paramInfo->{onchangeSubmit},
807                               defined($override) ? $param : undef,
808                               defined($override) ? $override->{$param} : undef
809                         );
810         if ( defined($paramInfo->{comment}) ) {
811             my $topDir = $bpc->TopDir;
812             my $text = eval("qq($Lang->{$paramInfo->{comment}})");
813             $tblContent .= <<EOF;
814 <tr><td colspan="2" class="editComment">$text</td></tr>
815 EOF
816         }
817     }
818
819     #
820     # Emit a summary of all the errors
821     #
822     my $errorTxt;
823
824     if ( %$errors ) {
825         $errorTxt .= <<EOF;
826 <tr><td colspan="2" class="border"><span class="editError">$Lang->{CfgEdit_Error_No_Save}</span></td></tr>
827 EOF
828     }
829
830     foreach my $param ( sort(keys(%$errors)) ) {
831         $errorTxt .= <<EOF;
832 <tr><td colspan="2" class="border"><span class="editError">$errors->{$param}</span></td></tr>
833 EOF
834     }
835
836     $content .= <<EOF;
837 $errorTxt
838 $tblContent
839 </table>
840 EOF
841
842     #
843     # Emit all the remaining editable config settings as hidden values
844     #
845     foreach my $param ( keys(%ConfigMeta) ) {
846         next if ( $doneParam->{$param} );
847         next if ( $userHost
848                       && (!defined($bpc->{Conf}{CgiUserConfigEdit}{$param})
849                          || (!$PrivAdmin
850                              && !$bpc->{Conf}{CgiUserConfigEdit}{$param})) );
851         $content .= fieldHiddenBuild($ConfigMeta{$param},
852                             $param,
853                             $newConf->{$param},
854                             "v"
855                         );
856         if ( defined($override) ) {
857             $content .= <<EOF;
858 <input type="hidden" name="override_$param" value="$override->{$param}">
859 EOF
860         }
861         $doneParam->{$param} = 1;
862     }
863
864     if ( defined($In{menu}) || $In{saveAction} eq "Save" ) {
865         if ( $In{saveAction} eq "Save" && !$userHost ) {
866             #
867             # Emit the new settings as orig_zZ_ parameters
868             #
869             $doneParam = {};
870             foreach my $param ( keys(%ConfigMeta) ) {
871                 next if ( $doneParam->{$param} );
872                 next if ( $userHost
873                           && (!defined($bpc->{Conf}{CgiUserConfigEdit}{$param})
874                              || (!$PrivAdmin
875                                 && !$bpc->{Conf}{CgiUserConfigEdit}{$param})) );
876                 $contentHidden .= fieldHiddenBuild($ConfigMeta{$param},
877                                         $param,
878                                         $newConf->{$param},
879                                         "orig",
880                                     );
881                 $doneParam->{$param} = 1;
882                 $In{modified} = 0;
883             }
884         } else {
885             #
886             # Just switching menus: copy all the orig_zZ_ input parameters
887             #
888             foreach my $var ( keys(%In) ) {
889                 next if ( $var !~ /^orig_zZ_/ );
890                 my $val = decode_utf8($In{$var});
891                 $contentHidden .= <<EOF;
892 <input type="hidden" name="$var" value="${EscHTML($val)}">
893 EOF
894             }
895         }
896     } else {
897         #
898         # First time: emit all the original config settings
899         #
900         $doneParam = {};
901         foreach my $param ( keys(%ConfigMeta) ) {
902             next if ( $doneParam->{$param} );
903             next if ( $userHost
904                           && (!defined($bpc->{Conf}{CgiUserConfigEdit}{$param})
905                              || (!$PrivAdmin
906                                 && !$bpc->{Conf}{CgiUserConfigEdit}{$param})) );
907             $contentHidden .= fieldHiddenBuild($ConfigMeta{$param},
908                                     $param,
909                                     $mainConf->{$param},
910                                     "orig",
911                                 );
912             $doneParam->{$param} = 1;
913         }
914     }
915
916     $content .= <<EOF;
917 $contentHidden
918 </form>
919 </tr>
920 </table>
921 EOF
922
923     Header("Config Edit", $content);
924     Trailer();
925 }
926
927 sub fieldHiddenBuild
928 {
929     my($type, $varName, $varValue, $prefix) = @_;
930     my $content;
931
932     $type = { type => $type } if ( ref($type) ne "HASH" );
933
934     if ( $type->{type} eq "list" ) {
935         $varValue = [] if ( !defined($varValue) );
936         $varValue = [$varValue] if ( ref($varValue) ne "ARRAY" );
937
938         for ( my $i = 0 ; $i < @$varValue ; $i++ ) {
939             $content .= fieldHiddenBuild($type->{child}, "${varName}_zZ_$i",
940                                          $varValue->[$i], $prefix);
941         }
942     } elsif ( $type->{type} eq "hash" || $type->{type} eq "horizHash" ) {
943         $varValue = {} if ( ref($varValue) ne "HASH" );
944         my(@order, $childType);
945
946         if ( defined($type->{order}) ) {
947             @order = @{$type->{order}};
948         } elsif ( defined($type->{child}) ) {
949             @order = sort(keys(%{$type->{child}}));
950         } else {
951             @order = sort(keys(%$varValue));
952         }
953
954         foreach my $fld ( @order ) {
955             if ( defined($type->{child}) ) {
956                 $childType = $type->{child}{$fld};
957             } else {
958                 $childType = $type->{childType};
959                 #
960                 # emit list of fields since they are user-defined
961                 # rather than hard-coded
962                 #
963                 $content .= <<EOF;
964 <input type="hidden" name="vflds.$varName" value="${EscHTML($fld)}">
965 EOF
966             }
967             $content .= fieldHiddenBuild($childType, "${varName}_zZ_$fld",
968                                          $varValue->{$fld}, $prefix);
969         }
970     } elsif ( $type->{type} eq "shortlist" ) {
971         $varValue = [$varValue] if ( ref($varValue) ne "ARRAY" );
972         $varValue = join(", ", @$varValue);
973         $content .= <<EOF;
974 <input type="hidden" name="${prefix}_zZ_$varName" value="${EscHTML($varValue)}">
975 EOF
976     } else {
977         $content .= <<EOF;
978 <input type="hidden" name="${prefix}_zZ_$varName" value="${EscHTML($varValue)}">
979 EOF
980     }
981     return $content;
982 }
983
984 sub fieldEditBuild
985 {
986     my($type, $varName, $varValue, $errors, $level, $comment, $isError,
987        $onchangeSubmit, $overrideVar, $overrideSet) = @_;
988
989     my $content;
990     my $size = 50 - 10 * $level;
991     $type = { type => $type } if ( ref($type) ne "HASH" );
992
993     $size = $type->{size} if ( defined($type->{size}) );
994
995     #
996     # These fragments allow inline content to be turned on and off
997     #
998     # <tr><td colspan="2"><span id="id_$varName" style="display: none" class="editComment">$comment</span></td></tr>
999     # <tr><td class="border"><a href="javascript: displayHelp('$varName')">$varName</a>
1000     #
1001
1002     if ( $level == 0 ) {
1003         my $lcVarName = lc($varName);
1004         $content .= <<EOF;
1005 <tr><td class="border"><a href="?action=view&type=docs#item__conf_${lcVarName}_">$varName</a>
1006 EOF
1007         if ( defined($overrideVar) ) {
1008             my $override_checked = "";
1009             if ( !$isError && $In{deleteVar}      =~ /^\Q${varName}_zZ_/
1010                    || !$isError && $In{insertVar} =~ /^\Q${varName}\E(_zZ_|$)/
1011                    || !$isError && $In{addVar}    =~ /^\Q${varName}\E(_zZ_|$)/ ) {
1012                 $overrideSet = 1;
1013             }
1014             if ( $overrideSet ) {
1015                 $override_checked = "checked";
1016             }
1017             $content .= <<EOF;
1018 <br><input type="checkbox" name="override_$varName" $override_checked value="1" onClick="checkboxChange('$varName')">\&nbsp;${EscHTML($Lang->{CfgEdit_Button_Override})}
1019 EOF
1020         }
1021         $content .= "</td>\n";
1022     }
1023
1024     if ( $type->{type} eq "list" ) {
1025         $content .= "<td class=\"border\">\n";
1026         $varValue = [] if ( !defined($varValue) );
1027         $varValue = [$varValue] if ( ref($varValue) ne "ARRAY" );
1028         if ( !$isError && $In{deleteVar} =~ /^\Q${varName}_zZ_\E(\d+)$/
1029                 && $1 < @$varValue ) {
1030             #
1031             # User deleted entry in this array
1032             #
1033             splice(@$varValue, $1, 1) if ( @$varValue > 1 || $type->{emptyOk} );
1034             $In{deleteVar} = "";
1035         }
1036         if ( !$isError && $In{insertVar} =~ /^\Q${varName}_zZ_\E(\d+)$/
1037                 && $1 < @$varValue ) {
1038             #
1039             # User inserted entry in this array
1040             #
1041             splice(@$varValue, $1, 0, "")
1042                         if ( @$varValue > 1 || $type->{emptyOk} );
1043             $In{insertVar} = "";
1044         }
1045         if ( !$isError && $In{addVar} eq $varName ) {
1046             #
1047             # User added entry to this array
1048             #
1049             push(@$varValue, undef);
1050             $In{addVar} = "";
1051         }
1052         $content .= "<table border=\"1\" cellspacing=\"0\">\n";
1053         my $colspan;
1054
1055         if ( ref($type) eq "HASH" && ref($type->{child}) eq "HASH"
1056                     && $type->{child}{type} eq "horizHash" ) {
1057             my @order;
1058             if ( defined($type->{child}{order}) ) {
1059                 @order = @{$type->{child}{order}};
1060             } else {
1061                 @order = sort(keys(%{$type->{child}{child}}));
1062             }
1063             $content .= "<tr><td class=\"border\"></td>\n";
1064             for ( my $i = 0 ; $i < @order ; $i++ ) {
1065                 $content .= "<td class=\"tableheader\">$order[$i]</td>\n";
1066             }
1067             $colspan = @order + 1;
1068             $content .= "</tr>\n";
1069             for ( my $i = 0 ; $i < @$varValue ; $i++ ) {
1070                 if ( @$varValue > 1 || $type->{emptyOk} ) {
1071                     $content .= <<EOF;
1072 <td class="border">
1073 <input type="button" name="del_${varName}_zZ_$i" value="${EscHTML($Lang->{CfgEdit_Button_Delete})}"
1074     onClick="deleteSubmit('${varName}_zZ_$i')">
1075 </td>
1076 EOF
1077                 }
1078                 $content .= fieldEditBuild($type->{child}, "${varName}_zZ_$i",
1079                                   $varValue->[$i], $errors, $level + 1, undef,
1080                                   $isError, $onchangeSubmit,
1081                                   $overrideVar, $overrideSet);
1082                 $content .= "</tr>\n";
1083             }
1084         } else {
1085             for ( my $i = 0 ; $i < @$varValue ; $i++ ) {
1086                 $content .= <<EOF;
1087 <tr><td class="border">
1088 <input type="button" name="ins_${varName}_zZ_$i" value="${EscHTML($Lang->{CfgEdit_Button_Insert})}"
1089     onClick="insertSubmit('${varName}_zZ_$i')">
1090 EOF
1091                 if ( @$varValue > 1 || $type->{emptyOk} ) {
1092                     $content .= <<EOF;
1093 <input type="button" name="del_${varName}_zZ_$i" value="${EscHTML($Lang->{CfgEdit_Button_Delete})}"
1094     onClick="deleteSubmit('${varName}_zZ_$i')">
1095 EOF
1096                 }
1097                 $content .= "</td>\n";
1098                 $content .= fieldEditBuild($type->{child}, "${varName}_zZ_$i",
1099                                     $varValue->[$i], $errors, $level + 1, undef,
1100                                     $isError, $onchangeSubmit,
1101                                     $overrideVar, $overrideSet);
1102                 $content .= "</tr>\n";
1103             }
1104             $colspan = 2;
1105         }
1106         $content .= <<EOF;
1107 <tr><td class="border" colspan="$colspan"><input type="button" name="add_$varName" value="${EscHTML($Lang->{CfgEdit_Button_Add})}"
1108     onClick="addSubmit('$varName')"></td></tr>
1109 </table>
1110 EOF
1111         $content .= "</td>\n";
1112     } elsif ( $type->{type} eq "hash" ) {
1113         $content .= "<td class=\"border\">\n";
1114         $content .= "<table border=\"1\" cellspacing=\"0\">\n";
1115         $varValue = {} if ( ref($varValue) ne "HASH" );
1116
1117         if ( !$isError && !$type->{noKeyEdit}
1118                         && $In{deleteVar} !~ /^\Q${varName}_zZ_\E.*_zZ_/
1119                         && $In{deleteVar} =~ /^\Q${varName}_zZ_\E(.*)$/ ) {
1120             #
1121             # User deleted entry in this hash
1122             #
1123             delete($varValue->{$1}) if ( keys(%$varValue) > 1
1124                                             || $type->{emptyOk} );
1125             $In{deleteVar} = "";
1126         }
1127         if ( !$isError && !defined($type->{child})
1128                         && $In{addVar} eq $varName ) {
1129             #
1130             # User added entry to this array
1131             #
1132             $varValue->{$In{"addVarKey_$varName"}} = ""
1133                         if ( !defined($varValue->{$In{"addVarKey_$varName"}}) );
1134             $In{addVar} = "";
1135         }
1136         my(@order, $childType);
1137
1138         if ( defined($type->{order}) ) {
1139             @order = @{$type->{order}};
1140         } elsif ( defined($type->{child}) ) {
1141             @order = sort(keys(%{$type->{child}}));
1142         } else {
1143             @order = sort(keys(%$varValue));
1144         }
1145
1146         foreach my $fld ( @order ) {
1147             $content .= <<EOF;
1148 <tr><td class="border">$fld
1149 EOF
1150             if ( !$type->{noKeyEdit}
1151                     && (keys(%$varValue) > 1 || $type->{emptyOk}) ) {
1152                 $content .= <<EOF;
1153 <input type="submit" name="del_${varName}_zZ_$fld" value="${EscHTML($Lang->{CfgEdit_Button_Delete})}"
1154         onClick="deleteSubmit('${varName}_zZ_$fld')">
1155 EOF
1156             }
1157             if ( defined($type->{child}) ) {
1158                 $childType = $type->{child}{$fld};
1159             } else {
1160                 $childType = $type->{childType};
1161                 #
1162                 # emit list of fields since they are user-defined
1163                 # rather than hard-coded
1164                 #
1165                 $content .= <<EOF;
1166 <input type="hidden" name="vflds.$varName" value="${EscHTML($fld)}">
1167 EOF
1168             }
1169             $content .= "</td>\n";
1170             $content .= fieldEditBuild($childType, "${varName}_zZ_$fld",
1171                             $varValue->{$fld}, $errors, $level + 1, undef,
1172                             $isError, $onchangeSubmit,
1173                             $overrideVar, $overrideSet);
1174             $content .= "</tr>\n";
1175         }
1176
1177         if ( !$type->{noKeyEdit} ) {
1178             $content .= <<EOF;
1179 <tr><td class="border" colspan="2">
1180 $Lang->{CfgEdit_Button_New_Key}: <input type="text" class="editTextInput" name="addVarKey_$varName" size="20" maxlength="256" value="">
1181 <input type="button" name="add_$varName" value="${EscHTML($Lang->{CfgEdit_Button_Add})}" onClick="addSubmit('$varName', 1)">
1182 </td></tr>
1183 EOF
1184         }
1185         $content .= "</table>\n";
1186         $content .= "</td>\n";
1187     } elsif ( $type->{type} eq "horizHash" ) {
1188         $varValue = {} if ( ref($varValue) ne "HASH" );
1189         my(@order, $childType);
1190
1191         if ( defined($type->{order}) ) {
1192             @order = @{$type->{order}};
1193         } elsif ( defined($type->{child}) ) {
1194             @order = sort(keys(%{$type->{child}}));
1195         } else {
1196             @order = sort(keys(%$varValue));
1197         }
1198
1199         foreach my $fld ( @order ) {
1200             if ( defined($type->{child}) ) {
1201                 $childType = $type->{child}{$fld};
1202             } else {
1203                 $childType = $type->{childType};
1204                 #
1205                 # emit list of fields since they are user-defined
1206                 # rather than hard-coded
1207                 #
1208                 $content .= <<EOF;
1209 <input type="hidden" name="vflds.$varName" value="${EscHTML($fld)}">
1210 EOF
1211             }
1212             $content .= fieldEditBuild($childType, "${varName}_zZ_$fld",
1213                             $varValue->{$fld}, $errors, $level + 1, undef,
1214                             $isError, $onchangeSubmit,
1215                             $overrideVar, $overrideSet);
1216         }
1217     } else {
1218         $content .= "<td class=\"border\">\n";
1219         if ( $isError ) {
1220             #
1221             # If there was an error, we use the original post values
1222             # in %In, rather than the parsed values in $varValue.
1223             # This is so that the user's erroneous input is preserved.
1224             #
1225             $varValue = $In{"v_zZ_$varName"} if ( defined($In{"v_zZ_$varName"}) );
1226         }
1227         if ( defined($errors->{$varName}) ) {
1228             $content .= <<EOF;
1229 <span class="editError">$errors->{$varName}</span><br>
1230 EOF
1231         }
1232         my $onChange;
1233         if ( defined($overrideVar) ) {
1234             $onChange .= "checkboxSet('$overrideVar');";
1235         } else {
1236             $onChange .= "varChange('$varName');";
1237         }
1238         if ( $onchangeSubmit ) {
1239             $onChange .= "document.editForm.submit();";
1240         }
1241         if ( $onChange ne "" ) {
1242             $onChange = " onChange=\"$onChange\"";
1243         }
1244         if ( $varValue !~ /\n/ &&
1245                 ($type->{type} eq "integer"
1246                     || $type->{type} eq "string"
1247                     || $type->{type} eq "execPath"
1248                     || $type->{type} eq "shortlist"
1249                     || $type->{type} eq "float") ) {
1250             # simple input box
1251             if ( $type->{type} eq "shortlist" ) {
1252                 $varValue = [$varValue] if ( ref($varValue) ne "ARRAY" );
1253                 $varValue = join(", ", @$varValue);
1254             }
1255             my $textType = ($varName =~ /Passwd/) ? "password" : "text";
1256             $content .= <<EOF;
1257 <input type="$textType" class="editTextInput" name="v_zZ_$varName" size="$size" maxlength="256" value="${EscHTML($varValue)}"$onChange>
1258 EOF
1259         } elsif ( $type->{type} eq "boolean" ) {
1260             # checkbox
1261             my $checked = "checked" if ( $varValue );
1262             $content .= <<EOF;
1263 <input type="checkbox" name="v_zZ_$varName" $checked value="1"$onChange>
1264 EOF
1265         } elsif ( $type->{type} eq "select" ) {
1266             $content .= <<EOF;
1267 <select name="v_zZ_$varName"$onChange>
1268 EOF
1269             foreach my $option ( @{$type->{values}} ) {
1270                 my $sel = " selected" if ( $varValue eq $option );
1271                 $content .= "<option$sel>$option</option>\n";
1272             }
1273             $content .= "</select>\n";
1274         } else {
1275             # multi-line text area - count number of lines
1276             my $rowCnt = $varValue =~ tr/\n//;
1277             $rowCnt = 1 if ( $rowCnt < 1 );
1278             $content .= <<EOF;
1279 <textarea name="v_zZ_$varName" class="editTextArea" cols="$size" rows="$rowCnt"$onChange>${EscHTML($varValue)}</textarea>
1280 EOF
1281         }
1282         $content .= "</td>\n";
1283     }
1284     return $content;
1285 }
1286
1287 sub errorCheck
1288 {
1289     my $errors = {};
1290
1291     foreach my $param ( keys(%ConfigMeta) ) {
1292         fieldErrorCheck($ConfigMeta{$param}, $param, $errors);
1293     }
1294     return $errors;
1295 }
1296
1297 sub fieldErrorCheck
1298 {
1299     my($type, $varName, $errors) = @_;
1300
1301     $type = { type => $type } if ( ref($type) ne "HASH" );
1302
1303     if ( $type->{type} eq "list" ) {
1304         for ( my $i = 0 ; ; $i++ ) {
1305             last if ( fieldErrorCheck($type->{child}, "${varName}_zZ_$i", $errors) );
1306         }
1307     } elsif ( $type->{type} eq "hash" || $type->{type} eq "horizHash" ) {
1308         my(@order, $childType);
1309         my $ret;
1310
1311         if ( defined($type->{order}) ) {
1312             @order = @{$type->{order}};
1313         } elsif ( defined($type->{child}) ) {
1314             @order = sort(keys(%{$type->{child}}));
1315         } else {
1316             @order = split(/\0/, $In{"vflds.$varName"});
1317         }
1318         foreach my $fld ( @order ) {
1319             if ( defined($type->{child}) ) {
1320                 $childType = $type->{child}{$fld};
1321             } else {
1322                 $childType = $type->{childType};
1323             }
1324             $ret ||= fieldErrorCheck($childType, "${varName}_zZ_$fld", $errors);
1325         }
1326         return $ret;
1327     } else {
1328         $In{"v_zZ_$varName"} = "0" if ( $type->{type} eq "boolean"
1329                                         && $In{"v_zZ_$varName"} eq "" );
1330
1331         return 1 if ( !exists($In{"v_zZ_$varName"}) );
1332
1333         (my $var = $varName) =~ s/_zZ_/./g;
1334
1335         if ( $type->{type} eq "integer"
1336                 || $type->{type} eq "boolean" ) {
1337             if ( $In{"v_zZ_$varName"} !~ /^-?\d+\s*$/s
1338                             && $In{"v_zZ_$varName"} ne "" ) {
1339                 $errors->{$varName} = eval("qq{$Lang->{CfgEdit_Error__must_be_an_integer}}");
1340             }
1341         } elsif ( $type->{type} eq "float" ) {
1342             if ( $In{"v_zZ_$varName"} !~ /^-?\d*(\.\d*)?\s*$/s
1343                             && $In{"v_zZ_$varName"} ne "" ) {
1344                 $errors->{$varName}
1345                         = eval("qq{$Lang->{CfgEdit_Error__must_be_real_valued_number}}");
1346             }
1347         } elsif ( $type->{type} eq "shortlist" ) {
1348             my @vals = split(/[,\s]+/, $In{"v_zZ_$varName"});
1349             for ( my $i = 0 ; $i < @vals ; $i++ ) {
1350                 if ( $type->{child} eq "integer"
1351                         && $vals[$i] !~ /^-?\d+\s*$/s
1352                         && $vals[$i] ne "" ) {
1353                     my $k = $i + 1;
1354                     $errors->{$varName} = eval("qq{$Lang->{CfgEdit_Error__entry__must_be_an_integer}}");
1355                 } elsif ( $type->{child} eq "float"
1356                         && $vals[$i] !~ /^-?\d*(\.\d*)?\s*$/s
1357                         && $vals[$i] ne "" ) {
1358                     my $k = $i + 1;
1359                     $errors->{$varName} = eval("qq{$Lang->{CfgEdit_Error__entry__must_be_real_valued_number}}");
1360                 }
1361             }
1362         } elsif ( $type->{type} eq "select" ) {
1363             my $match = 0;
1364             foreach my $option ( @{$type->{values}} ) {
1365                 if ( $In{"v_zZ_$varName"} eq $option ) {
1366                     $match = 1;
1367                     last;
1368                 }
1369             }
1370             $errors->{$varName} = eval("qq{$Lang->{CfgEdit_Error__must_be_valid_option}}")
1371                             if ( !$match );
1372         } elsif ( $type->{type} eq "execPath" ) {
1373             if ( $In{"v_zZ_$varName"} ne "" && !-x $In{"v_zZ_$varName"} ) {
1374                 $errors->{$varName} = eval("qq{$Lang->{CfgEdit_Error__must_be_executable_program}}");
1375             }
1376         } else {
1377             #
1378             # $type->{type} eq "string": no error checking
1379             #
1380         }
1381     }
1382     return 0;
1383 }
1384
1385 sub inputParse
1386 {
1387     my($bpc, $userHost) = @_;
1388     my $conf     = {};
1389     my $override = {};
1390
1391     foreach my $param ( keys(%ConfigMeta) ) {
1392         my $value;
1393         next if ( $userHost
1394                       && (!defined($bpc->{Conf}{CgiUserConfigEdit}{$param})
1395                          || (!$PrivAdmin
1396                             && !$bpc->{Conf}{CgiUserConfigEdit}{$param})) );
1397         fieldInputParse($ConfigMeta{$param}, $param, \$value);
1398         $conf->{$param}     = $value;
1399         $override->{$param} = $In{"override_$param"};
1400     }
1401     return ($conf, $override);
1402 }
1403
1404 sub fieldInputParse
1405 {
1406     my($type, $varName, $value) = @_;
1407
1408     $type = { type => $type } if ( ref($type) ne "HASH" );
1409
1410     if ( $type->{type} eq "list" ) {
1411         $$value = [];
1412         for ( my $i = 0 ; ; $i++ ) {
1413             my $val;
1414             last if ( fieldInputParse($type->{child}, "${varName}_zZ_$i", \$val) );
1415             push(@$$value, $val);
1416         }
1417         $$value = undef if ( $type->{undefIfEmpty} && @$$value == 0 );
1418     } elsif ( $type->{type} eq "hash" || $type->{type} eq "horizHash" ) {
1419         my(@order, $childType);
1420         my $ret;
1421         $$value = {};
1422
1423         if ( defined($type->{order}) ) {
1424             @order = @{$type->{order}};
1425         } elsif ( defined($type->{child}) ) {
1426             @order = sort(keys(%{$type->{child}}));
1427         } else {
1428             @order = split(/\0/, $In{"vflds.$varName"});
1429         }
1430
1431         foreach my $fld ( @order ) {
1432             my $val;
1433             if ( defined($type->{child}) ) {
1434                 $childType = $type->{child}{$fld};
1435             } else {
1436                 $childType = $type->{childType};
1437             }
1438             $ret ||= fieldInputParse($childType, "${varName}_zZ_$fld", \$val);
1439             last if ( $ret );
1440             $$value->{$fld} = $val;
1441         }
1442         return $ret;
1443     } else {
1444         if ( $type->{type} eq "boolean" ) {
1445             $$value = 0 + $In{"v_zZ_$varName"};
1446         } elsif ( !exists($In{"v_zZ_$varName"}) ) {
1447             return 1;
1448         }
1449
1450         my $v = $In{"v_zZ_$varName"};
1451
1452         if ( $type->{type} eq "integer" ) {
1453             if ( $v =~ /^-?\d+\s*$/s || $v eq "" ) {
1454                 $$value = 0 + $v;
1455             } else {
1456                 # error value - keep in string form
1457                 $$value = $v;
1458             }
1459         } elsif ( $type->{type} eq "float" ) {
1460             if ( $v =~ /^-?\d*(\.\d*)?\s*$/s || $v eq "" ) {
1461                 $$value = 0 + $v;
1462             } else {
1463                 # error value - keep in string form
1464                 $$value = $v;
1465             }
1466         } elsif ( $type->{type} eq "shortlist" ) {
1467             $$value = [split(/[,\s]+/, $v)];
1468             if ( $type->{child} eq "float" ) {
1469                 foreach ( @$$value ) {
1470                     if ( /^-?\d*(\.\d*)?\s*$/s || $v eq "" ) {
1471                         $_ += 0;
1472                     }
1473                 }
1474             } elsif ( $type->{child} eq "integer"
1475                         || $type->{child} eq "boolean" ) {
1476                 foreach ( @$$value ) {
1477                     if ( /^-?\d+\s*$/s || $v eq "" ) {
1478                         $_ += 0;
1479                     }
1480                 }
1481             }
1482         } else {
1483             $$value = decode_utf8($In{"v_zZ_$varName"});
1484             $$value =~ s/\r\n/\n/g;
1485             # remove leading space from exec paths
1486             $$value =~ s/^\s+// if ( $type->{type} eq "execPath" );
1487         }
1488         $$value = undef if ( $type->{undefIfEmpty} && $$value eq "" );
1489     }
1490     return 0;
1491 }
1492
1493 sub configDiffMesg
1494 {
1495     my($host, $oldConf, $newConf) = @_;
1496     my $mesg;
1497     my $conf;
1498
1499     if ( $host ne "" ) {
1500         $conf = "host $host config";
1501     } else {
1502         $conf = "main config";
1503     }
1504
1505     foreach my $p ( keys(%ConfigMeta) ) {
1506         if ( !exists($oldConf->{$p}) && !exists($newConf->{$p}) ) {
1507             next;
1508         } elsif ( exists($oldConf->{$p}) && !exists($newConf->{$p}) ) {
1509             $mesg .= eval("qq($Lang->{CfgEdit_Log_Delete_param})");
1510         } elsif ( !exists($oldConf->{$p}) && exists($newConf->{$p}) ) {
1511             my $dump = Data::Dumper->new([$newConf->{$p}]);
1512             $dump->Indent(0);
1513             $dump->Sortkeys(1);
1514             $dump->Terse(1);
1515             my $value = $dump->Dump;
1516             $value =~ s/\n/\\n/g;
1517             $value =~ s/\r/\\r/g;
1518             $mesg .= eval("qq($Lang->{CfgEdit_Log_Add_param_value})");
1519         } else {
1520             my $dump = Data::Dumper->new([$newConf->{$p}]);
1521             $dump->Indent(0);
1522             $dump->Sortkeys(1);
1523             $dump->Terse(1);
1524             my $valueNew = $dump->Dump;
1525
1526             my $v = $oldConf->{$p};
1527             if ( ref($newConf->{$p}) eq "ARRAY" && ref($v) eq "" ) {
1528                 $v = [$v];
1529             }
1530             $dump = Data::Dumper->new([$v]);
1531             $dump->Indent(0);
1532             $dump->Sortkeys(1);
1533             $dump->Terse(1);
1534             my $valueOld = $dump->Dump;
1535
1536             (my $valueNew2 = $valueNew) =~ s/['\n\r]//g;
1537             (my $valueOld2 = $valueOld) =~ s/['\n\r]//g;
1538
1539             next if ( $valueOld2 eq $valueNew2 );
1540
1541             $valueNew =~ s/\n/\\n/g;
1542             $valueOld =~ s/\n/\\n/g;
1543             $valueNew =~ s/\r/\\r/g;
1544             $valueOld =~ s/\r/\\r/g;
1545
1546             $mesg .= eval("qq($Lang->{CfgEdit_Log_Change_param_value})");
1547         }
1548     }
1549     return $mesg;
1550 }
1551
1552 sub hostsDiffMesg
1553 {
1554     my($hostsNew) = @_;
1555     my $hostsOld = $bpc->HostInfoRead();
1556     my($mesg, $hostChange);
1557
1558     foreach my $host ( keys(%$hostsOld) ) {
1559         if ( !defined($hostsNew->{$host}) ) {
1560             $mesg .= eval("qq($Lang->{CfgEdit_Log_Host_Delete})");
1561             $hostChange++;
1562             next;
1563         }
1564         foreach my $key ( keys(%{$hostsNew->{$host}}) ) {
1565             next if ( $hostsNew->{$host}{$key} eq $hostsOld->{$host}{$key} );
1566             my $valueOld = $hostsOld->{$host}{$key};
1567             my $valueNew = $hostsNew->{$host}{$key};
1568             $mesg .= eval("qq($Lang->{CfgEdit_Log_Host_Change})");
1569             $hostChange++;
1570         }
1571     }
1572
1573     foreach my $host ( keys(%$hostsNew) ) {
1574         next if ( defined($hostsOld->{$host}) );
1575         my $dump = Data::Dumper->new([$hostsNew->{$host}]);
1576         $dump->Indent(0);
1577         $dump->Sortkeys(1);
1578         $dump->Terse(1);
1579         my $value = $dump->Dump;
1580         $value =~ s/\n/\\n/g;
1581         $value =~ s/\r/\\r/g;
1582         $mesg .= eval("qq($Lang->{CfgEdit_Log_Host_Add})");
1583         $hostChange++;
1584     }
1585     return ($mesg, $hostChange);
1586 }
1587
1588 1;