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