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