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