3.1.0 changes:
[BackupPC.git] / lib / BackupPC / CGI / EditConfig.pm
index dc831c2..edc4526 100644 (file)
@@ -10,7 +10,7 @@
 #   Craig Barratt  <cbarratt@users.sourceforge.net>
 #
 # COPYRIGHT
-#   Copyright (C) 2004  Craig Barratt
+#   Copyright (C) 2005-2007  Craig Barratt
 #
 #   This program is free software; you can redistribute it and/or modify
 #   it under the terms of the GNU General Public License as published by
@@ -28,7 +28,7 @@
 #
 #========================================================================
 #
-# Version 2.1.0beta2pl1, released 30 May 2004.
+# Version 3.1.0, released 25 Nov 2007.
 #
 # See http://backuppc.sourceforge.net.
 #
@@ -41,34 +41,35 @@ use BackupPC::CGI::Lib qw(:all);
 use BackupPC::Config::Meta qw(:all);
 use BackupPC::Storage;
 use Data::Dumper;
+use Encode;
 
 our %ConfigMenu = (
     server => {
-        text  => "Server",
+        text  => "CfgEdit_Title_Server",
         param => [
-            {text => "General Parameters"},
+            {text => "CfgEdit_Title_General_Parameters"},
             {name => "ServerHost"},
             {name => "BackupPCUser"},
             {name => "BackupPCUserVerify"},
             {name => "MaxOldLogFiles"},
             {name => "TrashCleanSleepSec"},
 
-            {text => "Wakeup Schedule"},
+            {text => "CfgEdit_Title_Wakeup_Schedule"},
             {name => "WakeupSchedule"},
 
-            {text => "Concurrent Jobs"},
+            {text => "CfgEdit_Title_Concurrent_Jobs"},
             {name => "MaxBackups"},
             {name => "MaxUserBackups"},
             {name => "MaxPendingCmds"},
             {name => "MaxBackupPCNightlyJobs"},
             {name => "BackupPCNightlyPeriod"},
 
-            {text => "Pool Filesystem Limits"},
+            {text => "CfgEdit_Title_Pool_Filesystem_Limits"},
            {name => "DfCmd"},
            {name => "DfMaxUsagePct"},
            {name => "HardLinkMax"},
 
-            {text => "Other Parameters"},
+            {text => "CfgEdit_Title_Other_Parameters"},
            {name => "UmaskMode"},
            {name => "MyPath"},
             {name => "DHCPAddressRanges"},
@@ -76,11 +77,11 @@ our %ConfigMenu = (
             {name => "ServerInitdPath"},
             {name => "ServerInitdStartCmd"},
 
-            {text => "Remote Apache Settings"},
+            {text => "CfgEdit_Title_Remote_Apache_Settings"},
             {name => "ServerPort"},
             {name => "ServerMesgSecret"},
 
-            {text => "Program Paths"},
+            {text => "CfgEdit_Title_Program_Paths"},
            {name => "SshPath"},
            {name => "NmbLookupPath"},
            {name => "PingPath"},
@@ -91,22 +92,35 @@ our %ConfigMenu = (
            {name => "GzipPath"},
            {name => "Bzip2Path"},
 
-            {text => "Install Paths"},
+            {text => "CfgEdit_Title_Install_Paths"},
+            #
+            # Can only edit TopDir and LogDir if we are in FHS mode.
+            # Otherwise they are hardcoded in lib/BackupPC/Lib.pm.
+            #
+            {name => "TopDir",
+                    visible => sub { return $_[1]->useFHS(); } },
+            {name => "LogDir",
+                    visible => sub { return $_[1]->useFHS(); } },
            {name => "CgiDir"},
-           {name => "InstallDir"},
+            #
+            # Cannot edit ConfDir or InstallDir, since the real value is hardcoded in
+            # lib/BackupPC/Lib.pm.
+            # {name => "ConfDir"},
+           # {name => "InstallDir"},
+            #
         ],
     },
     email => {
-        text  => "Email",
+        text  => "CfgEdit_Title_Email",
         param => [
-            {text => "Email settings"},
+            {text => "CfgEdit_Title_Email_settings"},
             {name => "SendmailPath"},
             {name => "EMailNotifyMinDays"},
             {name => "EMailFromUserName"},
             {name => "EMailAdminUserName"},
             {name => "EMailUserDestDomain"},
 
-            {text => "Email User Messages"},
+            {text => "CfgEdit_Title_Email_User_Messages"},
            {name => "EMailNoBackupEverSubj"},
            {name => "EMailNoBackupEverMesg"},
            {name => "EMailNotifyOldBackupDays"},
@@ -115,19 +129,17 @@ our %ConfigMenu = (
            {name => "EMailNotifyOldOutlookDays"},
            {name => "EMailOutlookBackupSubj"},
            {name => "EMailOutlookBackupMesg"},
+           {name => "EMailHeaders"},
         ],
     },
     cgi => {
-        text => "CGI",
+        text => "CfgEdit_Title_CGI",
         param => [
-           {text => "Admin Privileges"},
+           {text => "CfgEdit_Title_Admin_Privileges"},
            {name => "CgiAdminUserGroup"},
            {name => "CgiAdminUsers"},
 
-           {text => "Config Editing"},
-           {name => "CgiUserConfigEdit"},
-
-           {text => "Page Rendering"},
+           {text => "CfgEdit_Title_Page_Rendering"},
            {name => "Language"},
            {name => "CgiNavBarAdminAllHosts"},
            {name => "CgiSearchBoxEnable"},
@@ -138,25 +150,30 @@ our %ConfigMenu = (
            {name => "CgiExt2ContentType"},
            {name => "CgiCSSFile"},
 
-           {text => "Paths"},
+           {text => "CfgEdit_Title_Paths"},
            {name => "CgiURL"},
            {name => "CgiImageDir"},
            {name => "CgiImageDirURL"},
 
-           {text => "User URLs"},
+           {text => "CfgEdit_Title_User_URLs"},
            {name => "CgiUserHomePageCheck"},
            {name => "CgiUserUrlCreate"},
 
+           {text => "CfgEdit_Title_User_Config_Editing"},
+           {name => "CgiUserConfigEditEnable"},
+           {name => "CgiUserConfigEdit"},
         ],
     },
     xfer => {
-        text => "Xfer",
+        text => "CfgEdit_Title_Xfer",
         param => [
-            {text => "Xfer Settings"},
+            {text => "CfgEdit_Title_Xfer_Settings"},
             {name => "XferMethod", onchangeSubmit => 1},
             {name => "XferLogLevel"},
+            {name => "ClientCharset"},
+            {name => "ClientCharsetLegacy"},
 
-            {text => "Smb Settings",
+            {text => "CfgEdit_Title_Smb_Settings",
                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
             {name => "SmbShareName",
                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
@@ -165,17 +182,19 @@ our %ConfigMenu = (
             {name => "SmbSharePasswd",
                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
 
-            {text => "Tar Settings",
+            {text => "CfgEdit_Title_Tar_Settings",
                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
             {name => "TarShareName",
                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
 
-            {text => "Rsync Settings",
+            {text => "CfgEdit_Title_Rsync_Settings",
                 visible => sub { return $_[0]->{XferMethod} eq "rsync"; } },
-            {text => "Rsyncd Settings",
+            {text => "CfgEdit_Title_Rsyncd_Settings",
                 visible => sub { return $_[0]->{XferMethod} eq "rsyncd"; } },
             {name => "RsyncShareName",
                 visible => sub { return $_[0]->{XferMethod} =~ /rsync/; } },
+            {name => "RsyncdUserName",
+                visible => sub { return $_[0]->{XferMethod} eq "rsyncd"; } },
             {name => "RsyncdPasswd",
                 visible => sub { return $_[0]->{XferMethod} eq "rsyncd"; } },
             {name => "RsyncdAuthRequired",
@@ -183,7 +202,18 @@ our %ConfigMenu = (
             {name => "RsyncCsumCacheVerifyProb",
                 visible => sub { return $_[0]->{XferMethod} =~ /rsync/; } },
 
-            {text => "Archive Settings",
+            {text => "CfgEdit_Title_BackupPCd_Settings",
+                visible => sub { return $_[0]->{XferMethod} eq "backuppcd"; } },
+            {name => "BackupPCdShareName",
+                visible => sub { return $_[0]->{XferMethod} eq "backuppcd"; } },
+            {name => "BackupPCdPath",
+                visible => sub { return $_[0]->{XferMethod} eq "backuppcd"; } },
+            {name => "BackupPCdCmd",
+                visible => sub { return $_[0]->{XferMethod} eq "backuppcd"; } },
+            {name => "BackupPCdRestoreCmd",
+                visible => sub { return $_[0]->{XferMethod} eq "backuppcd"; } },
+
+            {text => "CfgEdit_Title_Archive_Settings",
                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
             {name => "ArchiveDest",
                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
@@ -194,14 +224,14 @@ our %ConfigMenu = (
             {name => "ArchiveSplit",
                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
 
-            {text => "Include/Exclude",
+            {text => "CfgEdit_Title_Include_Exclude",
                 visible => sub { return $_[0]->{XferMethod} ne "archive"; } },
             {name => "BackupFilesOnly",
                 visible => sub { return $_[0]->{XferMethod} ne "archive"; } },
             {name => "BackupFilesExclude",
                 visible => sub { return $_[0]->{XferMethod} ne "archive"; } },
 
-            {text => "Smb Paths/Commands",
+            {text => "CfgEdit_Title_Smb_Paths_Commands",
                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
             {name => "SmbClientPath",
                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
@@ -212,7 +242,7 @@ our %ConfigMenu = (
             {name => "SmbClientRestoreCmd",
                 visible => sub { return $_[0]->{XferMethod} eq "smb"; } },
 
-            {text => "Tar Paths/Commands",
+            {text => "CfgEdit_Title_Tar_Paths_Commands",
                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
             {name => "TarClientPath",
                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
@@ -225,9 +255,9 @@ our %ConfigMenu = (
             {name => "TarClientRestoreCmd",
                 visible => sub { return $_[0]->{XferMethod} eq "tar"; } },
 
-            {text => "Rsync Paths/Commands/Args",
+            {text => "CfgEdit_Title_Rsync_Paths_Commands_Args",
                 visible => sub { return $_[0]->{XferMethod} eq "rsync"; } },
-            {text => "Rsyncd Port/Args",
+            {text => "CfgEdit_Title_Rsyncd_Port_Args",
                 visible => sub { return $_[0]->{XferMethod} eq "rsyncd"; } },
             {name => "RsyncClientPath",
                 visible => sub { return $_[0]->{XferMethod} eq "rsync"; } },
@@ -242,7 +272,7 @@ our %ConfigMenu = (
             {name => "RsyncRestoreArgs",
                 visible => sub { return $_[0]->{XferMethod} =~ /rsync/; } },
 
-            {text => "Archive Paths/Commands",
+            {text => "CfgEdit_Title_Archive_Paths_Commands",
                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
             {name => "ArchiveClientCmd",
                 visible => sub { return $_[0]->{XferMethod} eq "archive"; } },
@@ -250,27 +280,29 @@ our %ConfigMenu = (
         ],
     },
     schedule => {
-        text => "Schedule",
+        text => "CfgEdit_Title_Schedule",
         param => [
-           {text => "Full Backups"},
+           {text => "CfgEdit_Title_Full_Backups"},
            {name => "FullPeriod"},
            {name => "FullKeepCnt"},
            {name => "FullKeepCntMin"},
            {name => "FullAgeMax"},
 
-           {text => "Incremental Backups"},
+           {text => "CfgEdit_Title_Incremental_Backups"},
            {name => "IncrPeriod"},
            {name => "IncrKeepCnt"},
            {name => "IncrKeepCntMin"},
            {name => "IncrAgeMax"},
+           {name => "IncrLevels"},
            {name => "IncrFill"},
 
-           {text => "Blackouts"},
+           {text => "CfgEdit_Title_Blackouts"},
+            {name => "BackupsDisable"},
             {name => "BlackoutBadPingLimit"},
             {name => "BlackoutGoodCnt"},
             {name => "BlackoutPeriods"},
 
-           {text => "Other"},
+           {text => "CfgEdit_Title_Other"},
            {name => "PartialAgeMax"},
            {name => "RestoreInfoKeepCnt"},
            {name => "ArchiveInfoKeepCnt"},
@@ -278,9 +310,9 @@ our %ConfigMenu = (
        ],
     },
     backup => {
-        text => "Backup Settings",
+        text => "CfgEdit_Title_Backup_Settings",
         param => [
-           {text => "Client Lookup"},
+           {text => "CfgEdit_Title_Client_Lookup"},
            {name => "ClientNameAlias"},
            {name => "NmbLookupCmd"},
            {name => "NmbLookupFindHostCmd"},
@@ -288,12 +320,12 @@ our %ConfigMenu = (
            {name => "PingCmd"},
            {name => "PingMaxMsec"},
            
-           {text => "Other"},
+           {text => "CfgEdit_Title_Other"},
            {name => "ClientTimeout"},
            {name => "MaxOldPerPCLogFiles"},
            {name => "CompressLevel"},
 
-           {text => "User Commands"},
+           {text => "CfgEdit_Title_User_Commands"},
            {name => "DumpPreUserCmd"},
            {name => "DumpPostUserCmd"},
            {name => "DumpPreShareCmd"},
@@ -302,8 +334,17 @@ our %ConfigMenu = (
            {name => "RestorePostUserCmd"},
            {name => "ArchivePreUserCmd"},
            {name => "ArchivePostUserCmd"},
+           {name => "UserCmdCheckStatus"},
        ],
     },
+    hosts => {
+        text => "CfgEdit_Title_Hosts",
+        param => [
+           {text    => "CfgEdit_Title_Hosts"},
+           {name    => "Hosts",
+             comment => "CfgEdit_Hosts_Comment"},
+        ],
+    },
 );
 
 sub action
@@ -318,34 +359,44 @@ sub action
     my $config_path = $host eq "" ? "$TopDir/conf/config.pl"
                                   : "$TopDir/pc/$host/config.pl";
 
-    my $Privileged = CheckPermission();
-    my $userHost = 1 if ( $Privileged && !$PrivAdmin && defined($host) );
+    my $Privileged = CheckPermission($host)
+                       && ($PrivAdmin || $Conf{CgiUserConfigEditEnable});
+    my $userHost = 1 if ( defined($host) );
+    my $debugText;
 
     if ( !$Privileged ) {
         ErrorExit(eval("qq{$Lang->{Only_privileged_users_can_edit_config_files}}"));
     }
 
-    if ( defined($In{menu}) || $In{editAction} eq "Save" ) {
+    if ( defined($In{menu}) || $In{saveAction} eq "Save" ) {
        $errors = errorCheck();
        if ( %$errors ) {
            #
            # If there are errors, then go back to the same menu
            #
-           $In{editAction} = "";
-            $In{newMenu} = "";
+           $In{saveAction} = "";
+            #$In{newMenu} = "";
        }
+        if ( (my $var = $In{overrideUncheck}) ne "" ) {
+            #
+            # a compound variable was unchecked; delete or
+            # add extra variables to make the shape the same.
+            #
+            #print STDERR Dumper(\%In);
+            foreach my $v ( keys(%In) ) {
+                if ( $v =~ /^v_zZ_(\Q$var\E(_zZ_.*|$))/ ) {
+                    delete($In{$v}) if ( !defined($In{"orig_zZ_$1"}) );
+                }
+                if ( $v =~ /^orig_zZ_(\Q$var\E(_zZ_.*|$))/ ) {
+                    $In{"v_zZ_$1"} = $In{$v};
+                }
+            }
+            delete($In{"vflds.$var"});
+        }
+
         ($newConf, $override) = inputParse($bpc, $userHost);
        $override = undef if ( $host eq "" );
 
-       #
-       # Copy all the orig_ input parameters
-       #
-       foreach my $var ( keys(%In) ) {
-           next if ( $var !~ /^orig_/ );
-           $contentHidden .= <<EOF;
-<input type="hidden" name="$var" value="${EscHTML($In{$var})}">
-EOF
-       }
     } else {
        #
        # First time: pick up the current config settings
@@ -358,28 +409,14 @@ EOF
                $override->{$param} = 1;
            }
        } else {
+            my $hostInfo = $bpc->HostInfoRead();
            $hostConf = {};
+            $mainConf->{Hosts} = [map($hostInfo->{$_}, sort(keys(%$hostInfo)))];
        }
        $newConf = { %$mainConf, %$hostConf };
-
-       #
-       # Emit all the original config settings
-       #
-       my $doneParam = {};
-        foreach my $param ( keys(%ConfigMeta) ) {
-            next if ( $doneParam->{$param} );
-            next if ( $userHost && !$bpc->{Conf}{CgiUserConfigEdit}{$param} );
-            $contentHidden .= fieldHiddenBuild($ConfigMeta{$param},
-                                    $param,
-                                    $mainConf->{$param},
-                                    "orig",
-                                );
-            $doneParam->{$param} = 1;
-       }
-
     }
 
-    if ( $In{editAction} ne "Save" && $In{newMenu} ne ""
+    if ( $In{saveAction} ne "Save" && $In{newMenu} ne ""
                    && defined($ConfigMenu{$In{newMenu}}) ) {
         $menu = $In{newMenu};
     }
@@ -389,7 +426,8 @@ EOF
         #
         # For a non-admin user editing the host config, we need to
         # figure out which subsets of the menu tree will be visible,
-        # based on what is enabled
+        # based on what is enabled.  Admin users can edit all the
+        # available per-host settings.
         #
         foreach my $m ( keys(%ConfigMenu) ) {
             my $enabled = 0;
@@ -403,10 +441,12 @@ EOF
                     $text = $n;
                     $mask[$text] = 1;
                 } else {
-                    if ( $bpc->{Conf}{CgiUserConfigEdit}{$param} ) {
+                    if ( $bpc->{Conf}{CgiUserConfigEdit}{$param}
+                          || (defined($bpc->{Conf}{CgiUserConfigEdit}{$param})
+                                && $PrivAdmin) ) {
                         $mask[$text] = 0 if ( $text >= 0 );
                         $mask[$n] = 0;
-                        $enabled = 1;
+                        $enabled ||= 1;
                     } else {
                         $mask[$n] = 1;
                     }
@@ -432,111 +472,132 @@ EOF
     my $groupText;
     foreach my $m ( keys(%ConfigMenu) ) {
         next if ( $menuDisable{$m}{top} );
-       my $text = $ConfigMenu{$m}{text};
+       my $text = eval("qq($Lang->{$ConfigMenu{$m}{text}})");
         if ( $m eq $menu ) {
             $groupText .= <<EOF;
-<td bgcolor="grey"><a href="javascript:menuSubmit('$m')"><b>$text</b></a></td>
+<td class="editTabSel"><a href="javascript:menuSubmit('$m')"><b>$text</b></a></td>
 EOF
         } else {
             $groupText .= <<EOF;
-<td><a href="javascript:menuSubmit('$m')">$text</a></td>
+<td class="editTabNoSel"><a href="javascript:menuSubmit('$m')">$text</a></td>
 EOF
         }
     }
 
     if ( $host eq "" ) {
-       $content .= <<EOF;
-${h1("Main Configuration Editor")}
-EOF
+       $content .= eval("qq($Lang->{CfgEdit_Header_Main})");
     } else {
-       $content .= <<EOF;
-${h1("Host $host Configuration Editor")}
-<p>
-Note: Check Override if you want to modify a value specific to this host.
-EOF
+       $content .= eval("qq($Lang->{CfgEdit_Header_Host})");
     }
 
-    my $saveDisplay = "block";
-    $saveDisplay = "none" if ( !$In{modified} );
+    my $saveStyle = "";
+    my $saveColor = "#ff0000";
+    
+    if ( $In{modified} && $In{saveAction} ne "Save" && !%$errors ) {
+        $saveStyle = "style=\"color:$saveColor\"";
+    } else {
+        $In{modified} = 0;
+    }
+
+    #
+    # Add action and host to the URL so the nav bar link is
+    # highlighted
+    #
+    my $url = "$MyURL?action=editConfig";
+    $url .= "&host=$host" if ( $host ne "" );
     $content .= <<EOF;
 <table border="0" cellpadding="2">
 <tr>$groupText</tr>
 <tr>
-<form method="post" name="form1" action="$MyURL">
+<form method="post" name="editForm" action="$url">
 <input type="hidden" name="host" value="$host">
 <input type="hidden" name="menu" value="$menu">
 <input type="hidden" name="newMenu" value="">
 <input type="hidden" name="modified" value="$In{modified}">
 <input type="hidden" name="deleteVar" value="">
 <input type="hidden" name="insertVar" value="">
+<input type="hidden" name="overrideUncheck" value="">
 <input type="hidden" name="addVar" value="">
 <input type="hidden" name="action" value="editConfig">
-<input type="submit" style="display: $saveDisplay" name="editAction" value="Save">
-$contentHidden
+<input type="hidden" name="saveAction" value="">
+<input type="button" class="editSaveButton" name="editAction"
+    value="${EscHTML($Lang->{CfgEdit_Button_Save})}" $saveStyle
+    onClick="saveSubmit();">
+<p>
 
 <script language="javascript" type="text/javascript">
 <!--
 
+    function saveSubmit()
+    {
+        if ( document.editForm.modified.value != 0 ) {
+            document.editForm.saveAction.value = 'Save';
+            document.editForm.submit();
+        }
+        return false;
+    }
+
     function deleteSubmit(varName)
     {
-        document.form1.deleteVar.value = varName;
-       document.form1.modified.value = 1;
-        document.form1.submit();
+        document.editForm.deleteVar.value = varName;
+       document.editForm.modified.value = 1;
+        document.editForm.submit();
         return;
     }
 
     function insertSubmit(varName)
     {
-        document.form1.insertVar.value = varName;
-       document.form1.modified.value = 1;
-        document.form1.submit();
+        document.editForm.insertVar.value = varName;
+       document.editForm.modified.value = 1;
+        document.editForm.submit();
         return;
     }
 
     function addSubmit(varName, checkKey)
     {
-        if ( checkKey && document.form1.addVarKey.value == "" ) {
+        if ( checkKey
+            && eval("document.editForm.addVarKey_" + varName + ".value") == "" ) {
             alert("New key must be non-empty");
             return;
         }
-        document.form1.addVar.value = varName;
-       document.form1.modified.value = 1;
-        document.form1.submit();
+        document.editForm.addVar.value = varName;
+       document.editForm.modified.value = 1;
+        document.editForm.submit();
         return;
     }
 
     function menuSubmit(menuName)
     {
-        document.form1.newMenu.value = menuName;
-        document.form1.submit();
+        document.editForm.newMenu.value = menuName;
+        document.editForm.submit();
     }
 
     function varChange(varName)
     {
-       document.form1.editAction.style.display = "block";
-       document.form1.modified.value = 1;
+       document.editForm.modified.value = 1;
+        document.editForm.editAction.style.color = '$saveColor';
     }
 
     function checkboxChange(varName)
     {
-       document.form1.editAction.style.display = "block";
-       document.form1.modified.value = 1;
+       document.editForm.modified.value = 1;
+        document.editForm.editAction.style.color = '$saveColor';
        // Do nothing if the checkbox is now set
-        if ( eval("document.form1.override_" + varName + ".checked") ) {
+        if ( eval("document.editForm.override_" + varName + ".checked") ) {
            return false;
        }
        var allVars = {};
-       var varRE  = new RegExp("^v_(" + varName + ".*)");
-       var origRE = new RegExp("^orig_(" + varName + ".*)");
-        for ( var i = 0 ; i < document.form1.elements.length ; i++ ) {
-           var e = document.form1.elements[i];
+       var varRE  = new RegExp("^v_zZ_(" + varName + ".*)");
+       var origRE = new RegExp("^orig_zZ_(" + varName + ".*)");
+        for ( var i = 0 ; i < document.editForm.elements.length ; i++ ) {
+           var e = document.editForm.elements[i];
            var re;
            if ( (re = varRE.exec(e.name)) != null ) {
                if ( allVars[re[1]] == null ) {
                    allVars[re[1]] = 0;
                }
                allVars[re[1]]++;
-               //debugMsg("found v_ match with " + re[1]);
+               //debugMsg("found v_zZ_ match with " + re[1]);
                //debugMsg("allVars[" + re[1] + "] = " + allVars[re[1]]);
            } else if ( (re = origRE.exec(e.name)) != null ) {
                if ( allVars[re[1]] == null ) {
@@ -551,25 +612,28 @@ $contentHidden
            if ( allVars[v] != 0 ) {
                //debugMsg("Not the same shape because of " + v);
                sameShape = 0;
-           }
+           } else {
+                // copy the original variable values
+               //debugMsg("setting " + v);
+               eval("document.editForm.v_zZ_" + v + ".value = document.editForm.orig_zZ_" + v + ".value");
+            }
        }
        if ( sameShape ) {
-           for ( v in allVars ) {
-               //debugMsg("setting " + v);
-               eval("document.form1.v_" + v + ".value = document.form1.orig_" + v + ".value");
-           }
            return true;
        } else {
-           document.form1.submit();
+            // need to rebuild the form since the compound variable
+            // has changed shape
+            document.editForm.overrideUncheck.value = varName;
+           document.editForm.submit();
            return false;
        }
     }
 
     function checkboxSet(varName)
     {
-       document.form1.editAction.style.display = "block";
-       document.form1.modified.value = 1;
-        eval("document.form1.override_" + varName + ".checked = 1;");
+       document.editForm.modified.value = 1;
+        document.editForm.editAction.style.color = '$saveColor';
+        eval("document.editForm.override_" + varName + ".checked = 1;");
         return false;
     }
 
@@ -593,7 +657,7 @@ $contentHidden
 //-->
 </script>
 
-<span id="debug"></span>
+<span id="debug">$debugText</span>
 
 EOF
 
@@ -602,6 +666,7 @@ EOF
 EOF
 
     my $doneParam = {};
+    my $tblContent;
 
     #
     # There is a special case of the user deleting just the field
@@ -611,7 +676,7 @@ EOF
     if ( $In{deleteVar} ne "" && %$errors > 0 ) {
         my $matchAll = 1;
         foreach my $v ( keys(%$errors) ) {
-            if ( $v ne $In{deleteVar} && $v !~ /^\Q$In{deleteVar}_/ ) {
+            if ( $v ne $In{deleteVar} && $v !~ /^\Q$In{deleteVar}_zZ_/ ) {
                 $matchAll = 0;
                 last;
             }
@@ -621,28 +686,87 @@ EOF
 
     my $isError = %$errors;
 
-    if ( !$isError && $In{editAction} eq "Save" ) {
-        my $mesg;
+    if ( !$isError && $In{saveAction} eq "Save" ) {
+        my($mesg, $err);
        if ( $host ne "" ) {
            $hostConf = $bpc->ConfigDataRead($host) if ( !defined($hostConf) );
-            $mesg = configDiffMesg($host, $hostConf, $newConf);
-           foreach my $param ( %$newConf ) {
-               $hostConf->{$param} = $newConf->{$param}
-                               if ( $override->{param} );
+            my %hostConf2 = %$hostConf;
+           foreach my $param ( keys(%$newConf) ) {
+                if ( $override->{$param} ) {
+                    $hostConf->{$param} = $newConf->{$param}
+                } else {
+                    delete($hostConf->{$param});
+                }
            }
-           $bpc->ConfigDataWrite($host, $hostConf);
+            $mesg = configDiffMesg($host, \%hostConf2, $hostConf);
+           $err .= $bpc->ConfigDataWrite($host, $hostConf);
        } else {
            $mainConf = $bpc->ConfigDataRead() if ( !defined($mainConf) );
-            $mesg = configDiffMesg(undef, $mainConf, $newConf);
+
+            my $hostsSave = [];
+            my($hostsNew, $allHosts, $copyConf);
+            foreach my $entry ( @{$newConf->{Hosts}} ) {
+                next if ( $entry->{host} eq "" );
+                $allHosts->{$entry->{host}} = 1;
+                $allHosts->{$1} = 1 if ( $entry->{host} =~ /(.+?)\s*=/ );
+            }
+            foreach my $entry ( @{$newConf->{Hosts}} ) {
+                next if ( $entry->{host} eq ""
+                           || defined($hostsNew->{$entry->{host}}) );
+                if ( $entry->{host} =~ /(.+?)\s*=\s*(.+)/ ) {
+                    if ( defined($allHosts->{$2}) ) {
+                        $entry->{host} = $1;
+                        $copyConf->{$1} = $2;
+                    } else {
+                        my $fullHost = $entry->{host};
+                        my $copyHost = $2;
+                        $err .= eval("qq($Lang->{CfgEdit_Error_Copy_host_does_not_exist})");
+                    }
+                }
+                push(@$hostsSave, $entry);
+                $hostsNew->{$entry->{host}} = $entry;
+            }
+            ($mesg, my $hostChange) = hostsDiffMesg($hostsNew);
+            $bpc->HostInfoWrite($hostsNew) if ( $hostChange );
+            foreach my $host ( keys(%$copyConf) ) {
+                #
+                # Currently host names are forced to lc when they
+                # are read from the hosts file.  Therefore we need
+                # to force the from and to hosts to lc.
+                #
+                my $confData = $bpc->ConfigDataRead(lc($copyConf->{$host}));
+                my $fromHost = $copyConf->{$host};
+                $err  .= $bpc->ConfigDataWrite(lc($host), $confData);
+                $mesg .= eval("qq($Lang->{CfgEdit_Log_Copy_host_config})");
+            }
+
+            delete($newConf->{Hosts});
+            $mesg .= configDiffMesg(undef, $mainConf, $newConf);
            $mainConf = { %$mainConf, %$newConf };
-           $bpc->ConfigDataWrite(undef, $mainConf);
+           $err .= $bpc->ConfigDataWrite(undef, $mainConf);
+            $newConf->{Hosts} = $hostsSave;
        }
+        if ( defined($err) ) {
+            $tblContent .= <<EOF;
+<tr><td colspan="2" class="border"><span class="editError">$err</span></td></tr>
+EOF
+        }
+        $bpc->ServerConnect();
         if ( $mesg ne "" ) {
-            $bpc->ServerConnect();
+            (my $mesgBR = $mesg) =~ s/\n/<br>\n/g;
+             # uncomment this if you want the changes to be displayed
+#            $tblContent .= <<EOF;
+#<tr><td colspan="2" class="border"><span class="editComment">$mesgBR</span></td></tr>
+#EOF
             foreach my $str ( split(/\n/, $mesg) ) {
-                $bpc->ServerMesg($str);
+                $bpc->ServerMesg("log $str") if ( $str ne "" );
             }
         }
+        #
+        # Tell the server to reload, unless we only changed
+        # a client config
+        #
+        $bpc->ServerMesg("server reload") if ( $host eq "" );
     }
 
     my @mask = @{$menuDisable{$menu}{mask} || []};
@@ -654,12 +778,13 @@ EOF
 
         next if ( $disabled || $menuDisable{$menu}{top} );
         if ( ref($paramInfo->{visible}) eq "CODE"
-                        && !&{$paramInfo->{visible}}($newConf) ) {
+                        && !&{$paramInfo->{visible}}($newConf, $bpc) ) {
             next;
         }
 
-       if ( defined(my $text = $paramInfo->{text}) ) {
-           $content .= <<EOF;
+       if ( defined($paramInfo->{text}) ) {
+            my $text = eval("qq($Lang->{$paramInfo->{text}})");
+           $tblContent .= <<EOF;
 <tr><td colspan="2" class="editHeader">$text</td></tr>
 EOF
            next;
@@ -669,36 +794,52 @@ EOF
        # TODO: get parameter documentation
        #
        my $comment = "";
-       $comment =~ s/\'//g;
-       $comment =~ s/\"//g;
-        $comment =~ s/\n/ /g;
+       #$comment =~ s/\'//g;
+       #$comment =~ s/\"//g;
+        #$comment =~ s/\n/ /g;
 
         $doneParam->{$param} = 1;
 
-        $content .= fieldEditBuild($ConfigMeta{$param},
-                                $param,
-                                $newConf->{$param},
-                                $errors,
-                                0,
-                                $comment,
-                                $isError,
-                                $paramInfo->{onchangeSubmit},
-                               defined($override) ? $param : undef,
-                               defined($override) ? $override->{$param} : undef
+        $tblContent .= fieldEditBuild($ConfigMeta{$param},
+                              $param,
+                              $newConf->{$param},
+                              $errors,
+                              0,
+                              $comment,
+                              $isError,
+                              $paramInfo->{onchangeSubmit},
+                             defined($override) ? $param : undef,
+                             defined($override) ? $override->{$param} : undef
                         );
+        if ( defined($paramInfo->{comment}) ) {
+            my $topDir = $bpc->TopDir;
+            my $text = eval("qq($Lang->{$paramInfo->{comment}})");
+           $tblContent .= <<EOF;
+<tr><td colspan="2" class="editComment">$text</td></tr>
+EOF
+        }
     }
 
     #
-    # Emit any remaining errors - should not happen
+    # Emit a summary of all the errors
     #
+    my $errorTxt;
+
+    if ( %$errors ) {
+       $errorTxt .= <<EOF;
+<tr><td colspan="2" class="border"><span class="editError">$Lang->{CfgEdit_Error_No_Save}</span></td></tr>
+EOF
+    }
+
     foreach my $param ( sort(keys(%$errors)) ) {
-       $content .= <<EOF;
-<tr><td colspan="2" class="border">$errors->{$param}</td></tr>
+       $errorTxt .= <<EOF;
+<tr><td colspan="2" class="border"><span class="editError">$errors->{$param}</span></td></tr>
 EOF
-       delete($errors->{$param});
     }
 
     $content .= <<EOF;
+$errorTxt
+$tblContent
 </table>
 EOF
 
@@ -707,7 +848,10 @@ EOF
     #
     foreach my $param ( keys(%ConfigMeta) ) {
         next if ( $doneParam->{$param} );
-        next if ( $userHost && !$bpc->{Conf}{CgiUserConfigEdit}{$param} );
+        next if ( $userHost
+                      && (!defined($bpc->{Conf}{CgiUserConfigEdit}{$param})
+                         || (!$PrivAdmin
+                             && !$bpc->{Conf}{CgiUserConfigEdit}{$param})) );
         $content .= fieldHiddenBuild($ConfigMeta{$param},
                             $param,
                             $newConf->{$param},
@@ -721,7 +865,60 @@ EOF
         $doneParam->{$param} = 1;
     }
 
+    if ( defined($In{menu}) || $In{saveAction} eq "Save" ) {
+        if ( $In{saveAction} eq "Save" && !$userHost ) {
+            #
+            # Emit the new settings as orig_zZ_ parameters
+            #
+            $doneParam = {};
+            foreach my $param ( keys(%ConfigMeta) ) {
+                next if ( $doneParam->{$param} );
+                next if ( $userHost
+                          && (!defined($bpc->{Conf}{CgiUserConfigEdit}{$param})
+                             || (!$PrivAdmin
+                                && !$bpc->{Conf}{CgiUserConfigEdit}{$param})) );
+                $contentHidden .= fieldHiddenBuild($ConfigMeta{$param},
+                                        $param,
+                                        $newConf->{$param},
+                                        "orig",
+                                    );
+                $doneParam->{$param} = 1;
+                $In{modified} = 0;
+            }
+        } else {
+            #
+            # Just switching menus: copy all the orig_zZ_ input parameters
+            #
+            foreach my $var ( keys(%In) ) {
+                next if ( $var !~ /^orig_zZ_/ );
+                my $val = decode_utf8($In{$var});
+                $contentHidden .= <<EOF;
+<input type="hidden" name="$var" value="${EscHTML($val)}">
+EOF
+            }
+       }
+    } else {
+       #
+       # First time: emit all the original config settings
+       #
+       $doneParam = {};
+        foreach my $param ( keys(%ConfigMeta) ) {
+            next if ( $doneParam->{$param} );
+            next if ( $userHost
+                          && (!defined($bpc->{Conf}{CgiUserConfigEdit}{$param})
+                             || (!$PrivAdmin
+                                && !$bpc->{Conf}{CgiUserConfigEdit}{$param})) );
+            $contentHidden .= fieldHiddenBuild($ConfigMeta{$param},
+                                    $param,
+                                    $mainConf->{$param},
+                                    "orig",
+                                );
+            $doneParam->{$param} = 1;
+       }
+    }
+
     $content .= <<EOF;
+$contentHidden
 </form>
 </tr>
 </table>
@@ -743,14 +940,16 @@ sub fieldHiddenBuild
         $varValue = [$varValue] if ( ref($varValue) ne "ARRAY" );
 
         for ( my $i = 0 ; $i < @$varValue ; $i++ ) {
-            $content .= fieldHiddenBuild($type->{child}, "${varName}_$i",
+            $content .= fieldHiddenBuild($type->{child}, "${varName}_zZ_$i",
                                          $varValue->[$i], $prefix);
         }
-    } elsif ( $type->{type} eq "hash" ) {
+    } elsif ( $type->{type} eq "hash" || $type->{type} eq "horizHash" ) {
         $varValue = {} if ( ref($varValue) ne "HASH" );
         my(@order, $childType);
 
-        if ( defined($type->{child}) ) {
+        if ( defined($type->{order}) ) {
+            @order = @{$type->{order}};
+        } elsif ( defined($type->{child}) ) {
             @order = sort(keys(%{$type->{child}}));
         } else {
             @order = sort(keys(%$varValue));
@@ -769,18 +968,18 @@ sub fieldHiddenBuild
 <input type="hidden" name="vflds.$varName" value="${EscHTML($fld)}">
 EOF
             }
-            $content .= fieldHiddenBuild($childType, "${varName}_$fld",
+            $content .= fieldHiddenBuild($childType, "${varName}_zZ_$fld",
                                          $varValue->{$fld}, $prefix);
         }
     } elsif ( $type->{type} eq "shortlist" ) {
        $varValue = [$varValue] if ( ref($varValue) ne "ARRAY" );
        $varValue = join(", ", @$varValue);
         $content .= <<EOF;
-<input type="hidden" name="${prefix}_$varName" value="${EscHTML($varValue)}">
+<input type="hidden" name="${prefix}_zZ_$varName" value="${EscHTML($varValue)}">
 EOF
     } else {
         $content .= <<EOF;
-<input type="hidden" name="${prefix}_$varName" value="${EscHTML($varValue)}">
+<input type="hidden" name="${prefix}_zZ_$varName" value="${EscHTML($varValue)}">
 EOF
     }
     return $content;
@@ -795,33 +994,42 @@ sub fieldEditBuild
     my $size = 50 - 10 * $level;
     $type = { type => $type } if ( ref($type) ne "HASH" );
 
+    $size = $type->{size} if ( defined($type->{size}) );
+
+    #
+    # These fragments allow inline content to be turned on and off
+    #
+    # <tr><td colspan="2"><span id="id_$varName" style="display: none" class="editComment">$comment</span></td></tr>
+    # <tr><td class="border"><a href="javascript: displayHelp('$varName')">$varName</a>
+    #
+
     if ( $level == 0 ) {
+        my $lcVarName = lc($varName);
        $content .= <<EOF;
-<tr id="id_$varName" class="optionalComment"><td colspan="2">$comment</td></tr>
-<tr><td class="border"><a href="javascript: displayHelp('$varName')">$varName</a>
+<tr><td class="border"><a href="?action=view&type=docs#item__conf_${lcVarName}_">$varName</a>
 EOF
        if ( defined($overrideVar) ) {
            my $override_checked = "";
-           if ( !$isError && $In{deleteVar}       =~ /^\Q${varName}_/
-                   || !$isError && $In{insertVar} =~ /^\Q${varName}\E(_|$)/
-                   || !$isError && $In{addVar}    =~ /^\Q${varName}\E(_|$)/ ) {
+           if ( !$isError && $In{deleteVar}      =~ /^\Q${varName}_zZ_/
+                  || !$isError && $In{insertVar} =~ /^\Q${varName}\E(_zZ_|$)/
+                  || !$isError && $In{addVar}    =~ /^\Q${varName}\E(_zZ_|$)/ ) {
                $overrideSet = 1;
            }
            if ( $overrideSet ) {
                $override_checked = "checked";
            }
             $content .= <<EOF;
-<br><input type="checkbox" name="override_$varName" $override_checked value="1" onClick="checkboxChange('$varName')">\&nbsp;Override
+<br><input type="checkbox" name="override_$varName" $override_checked value="1" onClick="checkboxChange('$varName')">\&nbsp;${EscHTML($Lang->{CfgEdit_Button_Override})}
 EOF
        }
        $content .= "</td>\n";
     }
 
-    $content .= "<td class=\"border\">\n";
     if ( $type->{type} eq "list" ) {
+        $content .= "<td class=\"border\">\n";
         $varValue = [] if ( !defined($varValue) );
         $varValue = [$varValue] if ( ref($varValue) ne "ARRAY" );
-        if ( !$isError && $In{deleteVar} =~ /^\Q${varName}_\E(\d+)$/
+        if ( !$isError && $In{deleteVar} =~ /^\Q${varName}_zZ_\E(\d+)$/
                 && $1 < @$varValue ) {
             #
             # User deleted entry in this array
@@ -829,7 +1037,7 @@ EOF
             splice(@$varValue, $1, 1) if ( @$varValue > 1 || $type->{emptyOk} );
             $In{deleteVar} = "";
         }
-        if ( !$isError && $In{insertVar} =~ /^\Q${varName}_\E(\d+)$/
+        if ( !$isError && $In{insertVar} =~ /^\Q${varName}_zZ_\E(\d+)$/
                 && $1 < @$varValue ) {
             #
             # User inserted entry in this array
@@ -846,37 +1054,75 @@ EOF
             $In{addVar} = "";
         }
         $content .= "<table border=\"1\" cellspacing=\"0\">\n";
+        my $colspan;
 
-        for ( my $i = 0 ; $i < @$varValue ; $i++ ) {
-            $content .= "<tr><td class=\"border\">\n";
-           if ( @$varValue > 1 || $type->{emptyOk} ) {
-               $content .= <<EOF;
-<input type="button" name="ins_${varName}_$i" value="Insert"
-    onClick="insertSubmit('${varName}_$i')">
-<input type="button" name="del_${varName}_$i" value="Delete"
-    onClick="deleteSubmit('${varName}_$i')">
-EOF
-           }
-            $content .= "</td>\n";
-            $content .= fieldEditBuild($type->{child}, "${varName}_$i",
-                                $varValue->[$i], $errors, $level + 1, undef,
-                               $isError, $onchangeSubmit,
-                               $overrideVar, $overrideSet);
+        if ( ref($type) eq "HASH" && ref($type->{child}) eq "HASH"
+                    && $type->{child}{type} eq "horizHash" ) {
+            my @order;
+            if ( defined($type->{child}{order}) ) {
+                @order = @{$type->{child}{order}};
+            } else {
+                @order = sort(keys(%{$type->{child}{child}}));
+            }
+            $content .= "<tr><td class=\"border\"></td>\n";
+            for ( my $i = 0 ; $i < @order ; $i++ ) {
+                $content .= "<td class=\"tableheader\">$order[$i]</td>\n";
+            }
+            $colspan = @order + 1;
             $content .= "</tr>\n";
+            for ( my $i = 0 ; $i < @$varValue ; $i++ ) {
+                if ( @$varValue > 1 || $type->{emptyOk} ) {
+                    $content .= <<EOF;
+<td class="border">
+<input type="button" name="del_${varName}_zZ_$i" value="${EscHTML($Lang->{CfgEdit_Button_Delete})}"
+    onClick="deleteSubmit('${varName}_zZ_$i')">
+</td>
+EOF
+                }
+                $content .= fieldEditBuild($type->{child}, "${varName}_zZ_$i",
+                                  $varValue->[$i], $errors, $level + 1, undef,
+                                  $isError, $onchangeSubmit,
+                                  $overrideVar, $overrideSet);
+                $content .= "</tr>\n";
+            }
+        } else {
+            for ( my $i = 0 ; $i < @$varValue ; $i++ ) {
+                $content .= <<EOF;
+<tr><td class="border">
+<input type="button" name="ins_${varName}_zZ_$i" value="${EscHTML($Lang->{CfgEdit_Button_Insert})}"
+    onClick="insertSubmit('${varName}_zZ_$i')">
+EOF
+                if ( @$varValue > 1 || $type->{emptyOk} ) {
+                    $content .= <<EOF;
+<input type="button" name="del_${varName}_zZ_$i" value="${EscHTML($Lang->{CfgEdit_Button_Delete})}"
+    onClick="deleteSubmit('${varName}_zZ_$i')">
+EOF
+                }
+                $content .= "</td>\n";
+                $content .= fieldEditBuild($type->{child}, "${varName}_zZ_$i",
+                                    $varValue->[$i], $errors, $level + 1, undef,
+                                    $isError, $onchangeSubmit,
+                                    $overrideVar, $overrideSet);
+                $content .= "</tr>\n";
+            }
+            $colspan = 2;
         }
         $content .= <<EOF;
-<tr><td class="border"><input type="button" name="add_$varName" value="Add"
+<tr><td class="border" colspan="$colspan"><input type="button" name="add_$varName" value="${EscHTML($Lang->{CfgEdit_Button_Add})}"
     onClick="addSubmit('$varName')"></td></tr>
 </table>
 EOF
+        $content .= "</td>\n";
     } elsif ( $type->{type} eq "hash" ) {
+        $content .= "<td class=\"border\">\n";
         $content .= "<table border=\"1\" cellspacing=\"0\">\n";
         $varValue = {} if ( ref($varValue) ne "HASH" );
 
         if ( !$isError && !$type->{noKeyEdit}
-                        && $In{deleteVar} =~ /^\Q${varName}_\E(\w+)$/ ) {
+                        && $In{deleteVar} !~ /^\Q${varName}_zZ_\E.*_zZ_/
+                        && $In{deleteVar} =~ /^\Q${varName}_zZ_\E(.*)$/ ) {
             #
-            # User deleted entry in this array
+            # User deleted entry in this hash
             #
             delete($varValue->{$1}) if ( keys(%$varValue) > 1
                                            || $type->{emptyOk} );
@@ -887,13 +1133,15 @@ EOF
             #
             # User added entry to this array
             #
-            $varValue->{$In{addVarKey}} = ""
-                            if ( !defined($varValue->{$In{addVarKey}}) );
+            $varValue->{$In{"addVarKey_$varName"}} = ""
+                        if ( !defined($varValue->{$In{"addVarKey_$varName"}}) );
             $In{addVar} = "";
         }
         my(@order, $childType);
 
-        if ( defined($type->{child}) ) {
+        if ( defined($type->{order}) ) {
+            @order = @{$type->{order}};
+        } elsif ( defined($type->{child}) ) {
             @order = sort(keys(%{$type->{child}}));
         } else {
             @order = sort(keys(%$varValue));
@@ -906,8 +1154,8 @@ EOF
             if ( !$type->{noKeyEdit}
                    && (keys(%$varValue) > 1 || $type->{emptyOk}) ) {
                 $content .= <<EOF;
-<input type="submit" name="del_${varName}_$fld" value="Delete"
-        onClick="deleteSubmit('${varName}_$fld')">
+<input type="submit" name="del_${varName}_zZ_$fld" value="${EscHTML($Lang->{CfgEdit_Button_Delete})}"
+        onClick="deleteSubmit('${varName}_zZ_$fld')">
 EOF
             }
             if ( defined($type->{child}) ) {
@@ -923,7 +1171,7 @@ EOF
 EOF
             }
             $content .= "</td>\n";
-            $content .= fieldEditBuild($childType, "${varName}_$fld",
+            $content .= fieldEditBuild($childType, "${varName}_zZ_$fld",
                             $varValue->{$fld}, $errors, $level + 1, undef,
                            $isError, $onchangeSubmit,
                            $overrideVar, $overrideSet);
@@ -933,35 +1181,66 @@ EOF
         if ( !$type->{noKeyEdit} ) {
             $content .= <<EOF;
 <tr><td class="border" colspan="2">
-New key: <input type="text" name="addVarKey" size="20" maxlength="256" value="">
-<input type="button" name="add_$varName" value="Add" onClick="addSubmit('$varName', 1)">
+$Lang->{CfgEdit_Button_New_Key}: <input type="text" class="editTextInput" name="addVarKey_$varName" size="20" maxlength="256" value="">
+<input type="button" name="add_$varName" value="${EscHTML($Lang->{CfgEdit_Button_Add})}" onClick="addSubmit('$varName', 1)">
 </td></tr>
 EOF
         }
         $content .= "</table>\n";
+        $content .= "</td>\n";
+    } elsif ( $type->{type} eq "horizHash" ) {
+        $varValue = {} if ( ref($varValue) ne "HASH" );
+        my(@order, $childType);
+
+        if ( defined($type->{order}) ) {
+            @order = @{$type->{order}};
+        } elsif ( defined($type->{child}) ) {
+            @order = sort(keys(%{$type->{child}}));
+        } else {
+            @order = sort(keys(%$varValue));
+        }
+
+        foreach my $fld ( @order ) {
+            if ( defined($type->{child}) ) {
+                $childType = $type->{child}{$fld};
+            } else {
+                $childType = $type->{childType};
+                #
+                # emit list of fields since they are user-defined
+                # rather than hard-coded
+                #
+                $content .= <<EOF;
+<input type="hidden" name="vflds.$varName" value="${EscHTML($fld)}">
+EOF
+            }
+            $content .= fieldEditBuild($childType, "${varName}_zZ_$fld",
+                            $varValue->{$fld}, $errors, $level + 1, undef,
+                           $isError, $onchangeSubmit,
+                           $overrideVar, $overrideSet);
+        }
     } else {
+        $content .= "<td class=\"border\">\n";
         if ( $isError ) {
             #
             # If there was an error, we use the original post values
             # in %In, rather than the parsed values in $varValue.
             # This is so that the user's erroneous input is preserved.
             #
-            $varValue = $In{"v_$varName"} if ( defined($In{"v_$varName"}) );
+            $varValue = $In{"v_zZ_$varName"} if ( defined($In{"v_zZ_$varName"}) );
         }
         if ( defined($errors->{$varName}) ) {
             $content .= <<EOF;
-$errors->{$varName}<br>
+<span class="editError">$errors->{$varName}</span><br>
 EOF
-           delete($errors->{$varName});
         }
         my $onChange;
        if ( defined($overrideVar) ) {
             $onChange .= "checkboxSet('$overrideVar');";
        } else {
-            $onChange .= "varChange('$overrideVar');";
+            $onChange .= "varChange('$varName');";
        }
         if ( $onchangeSubmit ) {
-            $onChange .= "document.form1.submit();";
+            $onChange .= "document.editForm.submit();";
         }
        if ( $onChange ne "" ) {
             $onChange = " onChange=\"$onChange\"";
@@ -969,6 +1248,7 @@ EOF
         if ( $varValue !~ /\n/ &&
                ($type->{type} eq "integer"
                    || $type->{type} eq "string"
+                   || $type->{type} eq "execPath"
                    || $type->{type} eq "shortlist"
                    || $type->{type} eq "float") ) {
             # simple input box
@@ -976,18 +1256,19 @@ EOF
                $varValue = [$varValue] if ( ref($varValue) ne "ARRAY" );
                $varValue = join(", ", @$varValue);
            }
+            my $textType = ($varName =~ /Passwd/) ? "password" : "text";
             $content .= <<EOF;
-<input type="text" name="v_$varName" size="$size" maxlength="256" value="${EscHTML($varValue)}"$onChange>
+<input type="$textType" class="editTextInput" name="v_zZ_$varName" size="$size" maxlength="256" value="${EscHTML($varValue)}"$onChange>
 EOF
         } elsif ( $type->{type} eq "boolean" ) {
             # checkbox
             my $checked = "checked" if ( $varValue );
             $content .= <<EOF;
-<input type="checkbox" name="v_$varName" $checked value="1">
+<input type="checkbox" name="v_zZ_$varName" $checked value="1"$onChange>
 EOF
         } elsif ( $type->{type} eq "select" ) {
             $content .= <<EOF;
-<select name="v_$varName"$onChange>
+<select name="v_zZ_$varName"$onChange>
 EOF
             foreach my $option ( @{$type->{values}} ) {
                 my $sel = " selected" if ( $varValue eq $option );
@@ -999,11 +1280,11 @@ EOF
            my $rowCnt = $varValue =~ tr/\n//;
            $rowCnt = 1 if ( $rowCnt < 1 );
             $content .= <<EOF;
-<textarea name="v_$varName" cols="$size" rows="$rowCnt"$onChange>${EscHTML($varValue)}</textarea>
+<textarea name="v_zZ_$varName" class="editTextArea" cols="$size" rows="$rowCnt"$onChange>${EscHTML($varValue)}</textarea>
 EOF
         }
+        $content .= "</td>\n";
     }
-    $content .= "</td>\n";
     return $content;
 }
 
@@ -1025,13 +1306,15 @@ sub fieldErrorCheck
 
     if ( $type->{type} eq "list" ) {
         for ( my $i = 0 ; ; $i++ ) {
-            last if ( fieldErrorCheck($type->{child}, "${varName}_$i", $errors) );
+            last if ( fieldErrorCheck($type->{child}, "${varName}_zZ_$i", $errors) );
         }
-    } elsif ( $type->{type} eq "hash" ) {
+    } elsif ( $type->{type} eq "hash" || $type->{type} eq "horizHash" ) {
         my(@order, $childType);
         my $ret;
 
-        if ( defined($type->{child}) ) {
+        if ( defined($type->{order}) ) {
+            @order = @{$type->{order}};
+        } elsif ( defined($type->{child}) ) {
             @order = sort(keys(%{$type->{child}}));
         } else {
             @order = split(/\0/, $In{"vflds.$varName"});
@@ -1042,51 +1325,58 @@ sub fieldErrorCheck
             } else {
                 $childType = $type->{childType};
             }
-            $ret ||= fieldErrorCheck($childType, "${varName}_$fld", $errors);
+            $ret ||= fieldErrorCheck($childType, "${varName}_zZ_$fld", $errors);
         }
         return $ret;
     } else {
-        return 1 if ( !exists($In{"v_$varName"}) );
+        $In{"v_zZ_$varName"} = "0" if ( $type->{type} eq "boolean"
+                                        && $In{"v_zZ_$varName"} eq "" );
+
+        return 1 if ( !exists($In{"v_zZ_$varName"}) );
+
+        (my $var = $varName) =~ s/_zZ_/./g;
 
         if ( $type->{type} eq "integer"
                 || $type->{type} eq "boolean" ) {
-            if ( $In{"v_$varName"} !~ /^-?\d+\s*$/s
-                           && $In{"v_$varName"} ne "" ) {
-                $errors->{$varName} = "Error: $varName must be an integer";
+            if ( $In{"v_zZ_$varName"} !~ /^-?\d+\s*$/s
+                           && $In{"v_zZ_$varName"} ne "" ) {
+                $errors->{$varName} = eval("qq{$Lang->{CfgEdit_Error__must_be_an_integer}}");
             }
         } elsif ( $type->{type} eq "float" ) {
-            if ( $In{"v_$varName"} !~ /^-?\d*(\.\d*)?\s*$/s
-                           && $In{"v_$varName"} ne "" ) {
+            if ( $In{"v_zZ_$varName"} !~ /^-?\d*(\.\d*)?\s*$/s
+                           && $In{"v_zZ_$varName"} ne "" ) {
                 $errors->{$varName}
-                        = "Error: $varName must be a real-valued number";
+                        = eval("qq{$Lang->{CfgEdit_Error__must_be_real_valued_number}}");
             }
         } elsif ( $type->{type} eq "shortlist" ) {
-           my @vals = split(/[,\s]+/, $In{"v_$varName"});
+           my @vals = split(/[,\s]+/, $In{"v_zZ_$varName"});
            for ( my $i = 0 ; $i < @vals ; $i++ ) {
                if ( $type->{child} eq "integer"
                        && $vals[$i] !~ /^-?\d+\s*$/s
                        && $vals[$i] ne "" ) {
                    my $k = $i + 1;
-                   $errors->{$varName} = "Error: $varName entry $k must"
-                                       . " be an integer";
+                   $errors->{$varName} = eval("qq{$Lang->{CfgEdit_Error__entry__must_be_an_integer}}");
                } elsif ( $type->{child} eq "float"
                        && $vals[$i] !~ /^-?\d*(\.\d*)?\s*$/s
                        && $vals[$i] ne "" ) {
                    my $k = $i + 1;
-                   $errors->{$varName} = "Error: $varName entry $k must"
-                                       . " be a real-valued number";
+                   $errors->{$varName} = eval("qq{$Lang->{CfgEdit_Error__entry__must_be_real_valued_number}}");
                }
            }
         } elsif ( $type->{type} eq "select" ) {
             my $match = 0;
             foreach my $option ( @{$type->{values}} ) {
-                if ( $In{"v_$varName"} eq $option ) {
+                if ( $In{"v_zZ_$varName"} eq $option ) {
                     $match = 1;
                     last;
                 }
             }
-            $errors->{$varName} = "Error: $varName must be a valid option"
+            $errors->{$varName} = eval("qq{$Lang->{CfgEdit_Error__must_be_valid_option}}")
                             if ( !$match );
+        } elsif ( $type->{type} eq "execPath" ) {
+            if ( $In{"v_zZ_$varName"} ne "" && !-x $In{"v_zZ_$varName"} ) {
+                $errors->{$varName} = eval("qq{$Lang->{CfgEdit_Error__must_be_executable_program}}");
+            }
         } else {
             #
             # $type->{type} eq "string": no error checking
@@ -1104,11 +1394,14 @@ sub inputParse
 
     foreach my $param ( keys(%ConfigMeta) ) {
         my $value;
-        next if ( $userHost && !$bpc->{Conf}{CgiUserConfigEdit}{$param} );
+        next if ( $userHost
+                      && (!defined($bpc->{Conf}{CgiUserConfigEdit}{$param})
+                         || (!$PrivAdmin
+                            && !$bpc->{Conf}{CgiUserConfigEdit}{$param})) );
         fieldInputParse($ConfigMeta{$param}, $param, \$value);
         $conf->{$param}     = $value;
         $override->{$param} = $In{"override_$param"};
-}
+    }
     return ($conf, $override);
 }
 
@@ -1122,16 +1415,18 @@ sub fieldInputParse
         $$value = [];
         for ( my $i = 0 ; ; $i++ ) {
             my $val;
-            last if ( fieldInputParse($type->{child}, "${varName}_$i", \$val) );
+            last if ( fieldInputParse($type->{child}, "${varName}_zZ_$i", \$val) );
             push(@$$value, $val);
         }
         $$value = undef if ( $type->{undefIfEmpty} && @$$value == 0 );
-    } elsif ( $type->{type} eq "hash" ) {
+    } elsif ( $type->{type} eq "hash" || $type->{type} eq "horizHash" ) {
         my(@order, $childType);
         my $ret;
         $$value = {};
 
-        if ( defined($type->{child}) ) {
+        if ( defined($type->{order}) ) {
+            @order = @{$type->{order}};
+        } elsif ( defined($type->{child}) ) {
             @order = sort(keys(%{$type->{child}}));
         } else {
             @order = split(/\0/, $In{"vflds.$varName"});
@@ -1144,33 +1439,55 @@ sub fieldInputParse
             } else {
                 $childType = $type->{childType};
             }
-            $ret ||= fieldInputParse($childType, "${varName}_$fld", \$val);
+            $ret ||= fieldInputParse($childType, "${varName}_zZ_$fld", \$val);
             last if ( $ret );
             $$value->{$fld} = $val;
         }
         return $ret;
     } else {
         if ( $type->{type} eq "boolean" ) {
-            $$value = 0 + $In{"v_$varName"};
-        } elsif ( !exists($In{"v_$varName"}) ) {
+            $$value = 0 + $In{"v_zZ_$varName"};
+        } elsif ( !exists($In{"v_zZ_$varName"}) ) {
             return 1;
         }
 
+        my $v = $In{"v_zZ_$varName"};
+
         if ( $type->{type} eq "integer" ) {
-            $$value = 0 + $In{"v_$varName"};
+            if ( $v =~ /^-?\d+\s*$/s || $v eq "" ) {
+                $$value = 0 + $v;
+            } else {
+                # error value - keep in string form
+                $$value = $v;
+            }
         } elsif ( $type->{type} eq "float" ) {
-            $$value = 0 + $In{"v_$varName"};
+            if ( $v =~ /^-?\d*(\.\d*)?\s*$/s || $v eq "" ) {
+                $$value = 0 + $v;
+            } else {
+                # error value - keep in string form
+                $$value = $v;
+            }
         } elsif ( $type->{type} eq "shortlist" ) {
-            $$value = [split(/[,\s]+/, $In{"v_$varName"})];
-            if ( $type->{child} eq "float"
-                    || $type->{child} eq "integer"
-                    || $type->{child} eq "boolean" ) {
+            $$value = [split(/[,\s]+/, $v)];
+            if ( $type->{child} eq "float" ) {
                 foreach ( @$$value ) {
-                    $_ += 0;
+                    if ( /^-?\d*(\.\d*)?\s*$/s || $v eq "" ) {
+                        $_ += 0;
+                    }
+                }
+            } elsif ( $type->{child} eq "integer"
+                        || $type->{child} eq "boolean" ) {
+                foreach ( @$$value ) {
+                    if ( /^-?\d+\s*$/s || $v eq "" ) {
+                        $_ += 0;
+                    }
                 }
             }
         } else {
-            $$value = $In{"v_$varName"};
+            $$value = decode_utf8($In{"v_zZ_$varName"});
+            $$value =~ s/\r\n/\n/g;
+            # remove leading space from exec paths
+            $$value =~ s/^\s+// if ( $type->{type} eq "execPath" );
         }
         $$value = undef if ( $type->{undefIfEmpty} && $$value eq "" );
     }
@@ -1193,14 +1510,16 @@ sub configDiffMesg
         if ( !exists($oldConf->{$p}) && !exists($newConf->{$p}) ) {
             next;
         } elsif ( exists($oldConf->{$p}) && !exists($newConf->{$p}) ) {
-            $mesg .= "log Deleted $p from $conf\n";
+            $mesg .= eval("qq($Lang->{CfgEdit_Log_Delete_param})");
         } elsif ( !exists($oldConf->{$p}) && exists($newConf->{$p}) ) {
             my $dump = Data::Dumper->new([$newConf->{$p}]);
             $dump->Indent(0);
             $dump->Sortkeys(1);
             $dump->Terse(1);
             my $value = $dump->Dump;
-            $mesg .= "log Added $p to $conf, set to $value\n";
+            $value =~ s/\n/\\n/g;
+            $value =~ s/\r/\\r/g;
+            $mesg .= eval("qq($Lang->{CfgEdit_Log_Add_param_value})");
         } else {
             my $dump = Data::Dumper->new([$newConf->{$p}]);
             $dump->Indent(0);
@@ -1218,11 +1537,56 @@ sub configDiffMesg
             $dump->Terse(1);
             my $valueOld = $dump->Dump;
 
-            $mesg .= "log Changed $p in $conf to $valueNew from $valueOld\n"
-                                    if ( $valueOld ne $valueNew );
+            (my $valueNew2 = $valueNew) =~ s/['\n\r]//g;
+            (my $valueOld2 = $valueOld) =~ s/['\n\r]//g;
+
+            next if ( $valueOld2 eq $valueNew2 );
+
+            $valueNew =~ s/\n/\\n/g;
+            $valueOld =~ s/\n/\\n/g;
+            $valueNew =~ s/\r/\\r/g;
+            $valueOld =~ s/\r/\\r/g;
+
+            $mesg .= eval("qq($Lang->{CfgEdit_Log_Change_param_value})");
         }
     }
     return $mesg;
 }
 
+sub hostsDiffMesg
+{
+    my($hostsNew) = @_;
+    my $hostsOld = $bpc->HostInfoRead();
+    my($mesg, $hostChange);
+
+    foreach my $host ( keys(%$hostsOld) ) {
+        if ( !defined($hostsNew->{$host}) ) {
+            $mesg .= eval("qq($Lang->{CfgEdit_Log_Host_Delete})");
+            $hostChange++;
+            next;
+        }
+        foreach my $key ( keys(%{$hostsNew->{$host}}) ) {
+            next if ( $hostsNew->{$host}{$key} eq $hostsOld->{$host}{$key} );
+            my $valueOld = $hostsOld->{$host}{$key};
+            my $valueNew = $hostsNew->{$host}{$key};
+            $mesg .= eval("qq($Lang->{CfgEdit_Log_Host_Change})");
+            $hostChange++;
+        }
+    }
+
+    foreach my $host ( keys(%$hostsNew) ) {
+        next if ( defined($hostsOld->{$host}) );
+        my $dump = Data::Dumper->new([$hostsNew->{$host}]);
+        $dump->Indent(0);
+        $dump->Sortkeys(1);
+        $dump->Terse(1);
+        my $value = $dump->Dump;
+        $value =~ s/\n/\\n/g;
+        $value =~ s/\r/\\r/g;
+        $mesg .= eval("qq($Lang->{CfgEdit_Log_Host_Add})");
+        $hostChange++;
+    }
+    return ($mesg, $hostChange);
+}
+
 1;