[Rt-commit] rt branch, 4.6/configindatabase-themed, created. rt-4.4.4-510-g1d87ddbb83

Michel Rodriguez michel at bestpractical.com
Fri Nov 8 08:32:38 EST 2019


The branch, 4.6/configindatabase-themed has been created
        at  1d87ddbb831682eb8c390a121dcb71bd64994648 (commit)

- Log -----------------------------------------------------------------
commit c9a5e3f65498b1801b123ba66d919f046ce8ed80
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Aug 15 16:01:59 2017 +0000

    Allow specifying size for Integer and String widgets

diff --git a/share/html/Widgets/Form/Integer b/share/html/Widgets/Form/Integer
index fa48d6071f..918fe88b0e 100644
--- a/share/html/Widgets/Form/Integer
+++ b/share/html/Widgets/Form/Integer
@@ -78,10 +78,11 @@ $DefaultLabel   => undef
 </%ARGS>
 
 <%METHOD InputOnly>
-<input type="text" name="<% $Name %>" value="<% $CurrentValue %>" class="form-control" />\
+<input type="text" name="<% $Name %>" size="<% $Size %>" value="<% $CurrentValue %>" class="form-control" />\
 <%ARGS>
 $Name
 $CurrentValue => '',
+$Size => 20
 </%ARGS>
 <%INIT>
 $CurrentValue = '' unless defined $CurrentValue;
diff --git a/share/html/Widgets/Form/String b/share/html/Widgets/Form/String
index 89da49761f..165daf2b9d 100644
--- a/share/html/Widgets/Form/String
+++ b/share/html/Widgets/Form/String
@@ -74,11 +74,12 @@ $DefaultLabel   => loc( 'Default: [_1]', $DefaultValue ),
 </%ARGS>
 
 <%METHOD InputOnly>
-<input type="<% $Type %>" name="<% $Name %>" value="<% $CurrentValue || '' %>" class="form-control" />\
+<input type="<% $Type %>" name="<% $Name %>" size="<% $Size %>" value="<% $CurrentValue || '' %>" class="form-control" />\
 <%ARGS>
 $Name
 $CurrentValue => '',
 $Type => 'text'
+$Size => 20
 </%ARGS>
 </%METHOD>
 

commit e48aa8f6ccfe8ce805c4aff42b637206ec9bbdd1
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Aug 15 16:02:18 2017 +0000

    Allow String widget to have a value of "0"
    
    Using $CurrentValue || '' means that 0 gets canonicalized to the empty
    string, which means you can't have a default of 0.
    
    This is necessary for RT::Extension::ConfigInDatabase's input field for
    $SetOutgoingMailFrom, which is a string that takes an email address or
    the special case value of "0".

diff --git a/share/html/Widgets/Form/String b/share/html/Widgets/Form/String
index 165daf2b9d..24cada9716 100644
--- a/share/html/Widgets/Form/String
+++ b/share/html/Widgets/Form/String
@@ -74,7 +74,7 @@ $DefaultLabel   => loc( 'Default: [_1]', $DefaultValue ),
 </%ARGS>
 
 <%METHOD InputOnly>
-<input type="<% $Type %>" name="<% $Name %>" size="<% $Size %>" value="<% $CurrentValue || '' %>" class="form-control" />\
+<input type="<% $Type %>" name="<% $Name %>" size="<% $Size %>" value="<% $CurrentValue // '' %>" class="form-control" />\
 <%ARGS>
 $Name
 $CurrentValue => '',

commit 12f9dcbca0cf7801a6a6d29f8133feed105ae5db
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Aug 15 16:07:59 2017 +0000

    Add RadioStyle option to Boolean widget
    
    If you use a Boolean widget with no Default, then the Yes/No/(Default)
    radio widget gets automatically downgraded to a checkbox. Instead, allow
    RadioStyle => 1 to present a Yes/No radio widget

diff --git a/share/html/Widgets/Form/Boolean b/share/html/Widgets/Form/Boolean
index e509f94955..786553d663 100644
--- a/share/html/Widgets/Form/Boolean
+++ b/share/html/Widgets/Form/Boolean
@@ -70,10 +70,11 @@ $Name         => undef,
 $Default        => 0,
 $DefaultValue   => 0,
 $DefaultLabel   => loc( 'Use default ([_1])', $DefaultValue? loc('Yes'): loc('No') ),
+$RadioStyle     => 0
 
 $CurrentValue => undef,
 </%ARGS>
-% unless ( $Default ) {
+% if ( !$Default && !$RadioStyle ) {
 <input type="hidden" name="<% $Name %>" value="0" />\
 <div class="custom-control custom-checkbox">
   <input type="checkbox" id="<% $Name %>" name="<% $Name %>" class="custom-control-input" value="1" <% $CurrentValue? ' checked="checked"': '' |n %>>
@@ -94,13 +95,14 @@ $CurrentValue => undef,
       <label class="custom-control-label" for="<% $Name %>-no"><&|/l&>No</&></label>
     </div>
   </div>
-
+% if ($Default) {
   <div class="col-md-auto">
     <div class="custom-control custom-radio">
       <input type="radio" id="<% $Name %>-empty" name="<% $Name %>" class="custom-control-input" value="__empty_value__" <% !defined $CurrentValue? ' checked="checked"': '' |n %>>
       <label class="custom-control-label" for="<% $Name %>-empty"><% $DefaultLabel %></label>
     </div>
   </div>
+% }
 </div>
 % }
 </%METHOD>

commit f2cd149e74849c23408023831021df2063a9e12a
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Aug 15 18:49:11 2017 +0000

    Make booleans with RadioStyle use true/false logic
    
    Rather than tri-value

diff --git a/share/html/Widgets/Form/Boolean b/share/html/Widgets/Form/Boolean
index 786553d663..3b7091b753 100644
--- a/share/html/Widgets/Form/Boolean
+++ b/share/html/Widgets/Form/Boolean
@@ -88,20 +88,26 @@ $CurrentValue => undef,
       <label class="custom-control-label" for="<% $Name %>-yes"><&|/l&>Yes</&></label>
     </div>
   </div>
-
+% if ($Default) {
   <div class="col-md-auto">
     <div class="custom-control custom-radio">
       <input type="radio" id="<% $Name %>-no" name="<% $Name %>" class="custom-control-input" value="0" <% defined $CurrentValue && !$CurrentValue? ' checked="checked"': '' |n %>>
       <label class="custom-control-label" for="<% $Name %>-no"><&|/l&>No</&></label>
     </div>
   </div>
-% if ($Default) {
   <div class="col-md-auto">
     <div class="custom-control custom-radio">
       <input type="radio" id="<% $Name %>-empty" name="<% $Name %>" class="custom-control-input" value="__empty_value__" <% !defined $CurrentValue? ' checked="checked"': '' |n %>>
       <label class="custom-control-label" for="<% $Name %>-empty"><% $DefaultLabel %></label>
     </div>
   </div>
+% } else {
+  <div class="col-md-auto">
+    <div class="custom-control custom-radio">
+      <input type="radio" id="<% $Name %>-no" name="<% $Name %>" class="custom-control-input" value="0" <% !$CurrentValue? ' checked="checked"': '' |n %>>
+      <label class="custom-control-label" for="<% $Name %>-no"><&|/l&>No</&></label>
+    </div>
+  </div>
 % }
 </div>
 % }

commit 46185f9e0af44b742c670ade284103056aa6ba1b
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Aug 15 18:59:45 2017 +0000

    DatabaseSetting schema updates

diff --git a/etc/acl.Pg b/etc/acl.Pg
index d393a6f5cb..a48373ec6d 100644
--- a/etc/acl.Pg
+++ b/etc/acl.Pg
@@ -66,6 +66,8 @@ sub acl {
         CustomRoles
         objectcustomroles_id_seq
         ObjectCustomRoles
+        databasesettings_id_seq
+        DatabaseSettings
     );
 
     my $db_user = RT->Config->Get('DatabaseUser');
diff --git a/etc/schema.Oracle b/etc/schema.Oracle
index 102f134ddd..b4ce7404e4 100644
--- a/etc/schema.Oracle
+++ b/etc/schema.Oracle
@@ -538,3 +538,20 @@ CREATE TABLE ObjectCustomRoles (
         LastUpdated     DATE
 );
 CREATE UNIQUE INDEX ObjectCustomRoles1 ON ObjectCustomRoles (ObjectId, CustomRole);
+
+CREATE SEQUENCE DatabaseSettings_seq;
+CREATE TABLE DatabaseSettings (
+    id              NUMBER(11,0)    CONSTRAINT DatabaseSettings_key PRIMARY KEY,
+    Name            VARCHAR2(255) CONSTRAINT DatabaseSettings_Name_Unique unique  NOT NULL,
+    Content         CLOB,
+    ContentType     VARCHAR2(80),
+    Disabled        NUMBER(11,0) DEFAULT 0 NOT NULL,
+    Creator         NUMBER(11,0)    DEFAULT 0 NOT NULL,
+    Created         DATE,
+    LastUpdatedBy   NUMBER(11,0)    DEFAULT 0 NOT NULL,
+    LastUpdated     DATE
+);
+
+CREATE UNIQUE INDEX DatabaseSettings1 ON DatabaseSettings (LOWER(Name));
+CREATE INDEX DatabaseSettings2 ON DatabaseSettings (Disabled);
+
diff --git a/etc/schema.Pg b/etc/schema.Pg
index 3836c26670..8c44a8f853 100644
--- a/etc/schema.Pg
+++ b/etc/schema.Pg
@@ -778,3 +778,21 @@ CREATE TABLE ObjectCustomRoles (
 );
 
 CREATE UNIQUE INDEX ObjectCustomRoles1 ON ObjectCustomRoles (ObjectId, CustomRole);
+
+CREATE SEQUENCE databasesettings_id_seq;
+CREATE TABLE DatabaseSettings (
+    id                integer         DEFAULT nextval('databasesettings_id_seq'),
+    Name              varchar(255)    NOT NULL,
+    Content           text            NULL,
+    ContentType       varchar(80)     NULL,
+    Disabled          integer         NOT NULL DEFAULT 0 ,
+    Creator           integer         NOT NULL DEFAULT 0,
+    Created           timestamp                DEFAULT NULL,
+    LastUpdatedBy     integer         NOT NULL DEFAULT 0,
+    LastUpdated       timestamp                DEFAULT NULL,
+    PRIMARY KEY (id)
+);
+
+CREATE UNIQUE INDEX DatabaseSettings1 ON DatabaseSettings (LOWER(Name));
+CREATE INDEX DatabaseSettings2 ON DatabaseSettings (Disabled);
+
diff --git a/etc/schema.SQLite b/etc/schema.SQLite
index b6d2867274..64d395a866 100644
--- a/etc/schema.SQLite
+++ b/etc/schema.SQLite
@@ -569,3 +569,19 @@ CREATE TABLE ObjectCustomRoles (
   PRIMARY KEY (id)
 );
 CREATE UNIQUE INDEX ObjectCustomRoles1 ON ObjectCustomRoles (ObjectId, CustomRole);
+
+CREATE TABLE DatabaseSettings (
+    id                INTEGER PRIMARY KEY,
+    Name              varchar(255)    collate NOCASE NOT NULL,
+    Content           longtext        collate NOCASE NULL,
+    ContentType       varchar(80)     collate NOCASE NULL,
+    Disabled          int2            NOT NULL DEFAULT 0,
+    Creator           int(11)         NOT NULL DEFAULT 0,
+    Created           timestamp                DEFAULT NULL,
+    LastUpdatedBy     int(11)         NOT NULL DEFAULT 0,
+    LastUpdated       timestamp                DEFAULT NULL
+);
+
+CREATE UNIQUE INDEX DatabaseSettings1 ON DatabaseSettings (Name);
+CREATE INDEX DatabaseSettings2 ON DatabaseSettings (Disabled);
+
diff --git a/etc/schema.mysql b/etc/schema.mysql
index cb87d86a3a..058f858709 100644
--- a/etc/schema.mysql
+++ b/etc/schema.mysql
@@ -559,3 +559,20 @@ CREATE TABLE ObjectCustomRoles (
 ) ENGINE=InnoDB CHARACTER SET utf8;
 
 CREATE UNIQUE INDEX ObjectCustomRoles1 ON ObjectCustomRoles (ObjectId, CustomRole);
+
+CREATE TABLE DatabaseSettings (
+    id                int(11)         NOT NULL AUTO_INCREMENT,
+    Name              varchar(255)    NOT NULL,
+    Content           longblob        NULL,
+    ContentType       varchar(80)     CHARACTER SET ascii NULL,
+    Disabled          int2            NOT NULL DEFAULT 0,
+    Creator           int(11)         NOT NULL DEFAULT 0,
+    Created           datetime                 DEFAULT NULL,
+    LastUpdatedBy     int(11)         NOT NULL DEFAULT 0,
+    LastUpdated       datetime                 DEFAULT NULL,
+    PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE UNIQUE INDEX DatabaseSettings1 ON DatabaseSettings (Name);
+CREATE UNIQUE INDEX DatabaseSettings2 ON DatabaseSettings (Disabled);
+
diff --git a/etc/upgrade/4.5.0/acl.Pg b/etc/upgrade/4.5.0/acl.Pg
new file mode 100644
index 0000000000..6a73f0cd45
--- /dev/null
+++ b/etc/upgrade/4.5.0/acl.Pg
@@ -0,0 +1,30 @@
+sub acl {
+    my $dbh = shift;
+
+    my @acls;
+    my @tables = qw (
+        databasesettings_id_seq
+        DatabaseSettings
+    );
+
+    my $db_user = RT->Config->Get('DatabaseUser');
+
+    my $sequence_right
+        = ( $dbh->{pg_server_version} >= 80200 )
+        ? "USAGE, SELECT, UPDATE"
+        : "SELECT, UPDATE";
+
+    foreach my $table (@tables) {
+        # Tables are upper-case, sequences are lowercase in @tables
+        if ( $table =~ /^[a-z]/ ) {
+            push @acls, "GRANT $sequence_right ON $table TO \"$db_user\";"
+        }
+        else {
+            push @acls, "GRANT SELECT, INSERT, UPDATE, DELETE ON $table TO \"$db_user\";"
+        }
+    }
+    return (@acls);
+}
+
+1;
+
diff --git a/etc/upgrade/4.5.0/schema.Oracle b/etc/upgrade/4.5.0/schema.Oracle
index b18ad9c979..17fed3dc1c 100644
--- a/etc/upgrade/4.5.0/schema.Oracle
+++ b/etc/upgrade/4.5.0/schema.Oracle
@@ -1 +1,16 @@
 ALTER TABLE Classes DROP( HotList );
+CREATE SEQUENCE DatabaseSettings_seq;
+CREATE TABLE DatabaseSettings (
+    id              NUMBER(11,0)    CONSTRAINT DatabaseSettings_key PRIMARY KEY,
+    Name            VARCHAR2(255) CONSTRAINT DatabaseSettings_Name_Unique unique  NOT NULL,
+    Content         CLOB,
+    ContentType     VARCHAR2(80),
+    Disabled        NUMBER(11,0) DEFAULT 0 NOT NULL,
+    Creator         NUMBER(11,0)    DEFAULT 0 NOT NULL,
+    Created         DATE,
+    LastUpdatedBy   NUMBER(11,0)    DEFAULT 0 NOT NULL,
+    LastUpdated     DATE
+);
+
+CREATE UNIQUE INDEX DatabaseSettings1 ON DatabaseSettings (LOWER(Name));
+CREATE INDEX DatabaseSettings2 ON DatabaseSettings (Disabled);
diff --git a/etc/upgrade/4.5.0/schema.Pg b/etc/upgrade/4.5.0/schema.Pg
index f7563efc51..0efa1411d0 100644
--- a/etc/upgrade/4.5.0/schema.Pg
+++ b/etc/upgrade/4.5.0/schema.Pg
@@ -1 +1,17 @@
 ALTER TABLE Classes DROP COLUMN HotList;
+CREATE SEQUENCE databasesettings_id_seq;
+CREATE TABLE DatabaseSettings (
+    id                integer         DEFAULT nextval('databasesettings_id_seq'),
+    Name              varchar(255)    NOT NULL,
+    Content           text            NULL,
+    ContentType       varchar(80)     NULL,
+    Disabled          integer         NOT NULL DEFAULT 0 ,
+    Creator           integer         NOT NULL DEFAULT 0,
+    Created           timestamp                DEFAULT NULL,
+    LastUpdatedBy     integer         NOT NULL DEFAULT 0,
+    LastUpdated       timestamp                DEFAULT NULL,
+    PRIMARY KEY (id)
+);
+
+CREATE UNIQUE INDEX DatabaseSettings1 ON DatabaseSettings (LOWER(Name));
+CREATE INDEX DatabaseSettings2 ON DatabaseSettings (Disabled);
diff --git a/etc/upgrade/4.5.0/schema.SQLite b/etc/upgrade/4.5.0/schema.SQLite
new file mode 100644
index 0000000000..a25a8c9b49
--- /dev/null
+++ b/etc/upgrade/4.5.0/schema.SQLite
@@ -0,0 +1,15 @@
+CREATE TABLE DatabaseSettings (
+    id                INTEGER PRIMARY KEY,
+    Name              varchar(255)    collate NOCASE NOT NULL,
+    Content           longtext        collate NOCASE NULL,
+    ContentType       varchar(80)     collate NOCASE NULL,
+    Disabled          int2            NOT NULL DEFAULT 0,
+    Creator           int(11)         NOT NULL DEFAULT 0,
+    Created           timestamp                DEFAULT NULL,
+    LastUpdatedBy     int(11)         NOT NULL DEFAULT 0,
+    LastUpdated       timestamp                DEFAULT NULL
+);
+
+CREATE UNIQUE INDEX DatabaseSettings1 ON DatabaseSettings (Name);
+CREATE INDEX DatabaseSettings2 ON DatabaseSettings (Disabled);
+
diff --git a/etc/upgrade/4.5.0/schema.mysql b/etc/upgrade/4.5.0/schema.mysql
index f7563efc51..36367a8729 100644
--- a/etc/upgrade/4.5.0/schema.mysql
+++ b/etc/upgrade/4.5.0/schema.mysql
@@ -1 +1,16 @@
 ALTER TABLE Classes DROP COLUMN HotList;
+CREATE TABLE DatabaseSettings (
+    id                int(11)         NOT NULL AUTO_INCREMENT,
+    Name              varchar(255)    NOT NULL,
+    Content           longblob        NULL,
+    ContentType       varchar(80)     CHARACTER SET ascii NULL,
+    Disabled          int2            NOT NULL DEFAULT 0,
+    Creator           int(11)         NOT NULL DEFAULT 0,
+    Created           datetime                 DEFAULT NULL,
+    LastUpdatedBy     int(11)         NOT NULL DEFAULT 0,
+    LastUpdated       datetime                 DEFAULT NULL,
+    PRIMARY KEY (id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE UNIQUE INDEX DatabaseSettings1 ON DatabaseSettings (Name);
+CREATE UNIQUE INDEX DatabaseSettings2 ON DatabaseSettings (Disabled);

commit 44d5d96568f69775f443541ddbcc4c99957a1799
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Aug 15 19:01:37 2017 +0000

    Add ORM classes for DatabaseSettings

diff --git a/lib/RT/DatabaseSetting.pm b/lib/RT/DatabaseSetting.pm
new file mode 100644
index 0000000000..fc38e87dae
--- /dev/null
+++ b/lib/RT/DatabaseSetting.pm
@@ -0,0 +1,417 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use strict;
+use warnings;
+use 5.10.1;
+
+package RT::DatabaseSetting;
+use base 'RT::Record';
+
+use Storable ();
+use MIME::Base64;
+use JSON ();
+
+=head1 NAME
+
+RT::DatabaseSetting - Represents a config setting
+
+=cut
+
+=head1 METHODS
+
+=head2 Create PARAMHASH
+
+Create takes a hash of values and creates a row in the database.  Available
+keys are:
+
+=over 4
+
+=item Name
+
+Must be unique.
+
+=item Content
+
+If you provide a reference, we will automatically serialize the data structure
+using L<Storable>. Otherwise any string is passed through as-is.
+
+=item ContentType
+
+Currently handles C<storable> or C<application/json>.
+
+=back
+
+Returns a tuple of (status, msg) on failure and (id, msg) on success.
+Also automatically propagates this config change to all server processes.
+
+=cut
+
+sub Create {
+    my $self = shift;
+    my %args = (
+        Name => '',
+        Content => '',
+        ContentType => '',
+        @_,
+    );
+
+    return (0, $self->loc("Permission Denied"))
+        unless $self->CurrentUserHasRight('SuperUser');
+
+    unless ( $args{'Name'} ) {
+        return ( 0, $self->loc("Must specify 'Name' attribute") );
+    }
+
+    my ( $id, $msg ) = $self->ValidateName( $args{'Name'} );
+    return ( 0, $msg ) unless $id;
+
+    my $meta = RT->Config->Meta($args{'Name'});
+    if ($meta->{Immutable}) {
+        return ( 0, $self->loc("You cannot update [_1] using database config; you must edit your site config", $args{'Name'}) );
+    }
+
+    if (ref ($args{'Content'}) ) {
+        ($args{'Content'}, my $error) = $self->_SerializeContent($args{'Content'}, $args{'Name'});
+        if ($error) {
+            return (0, $error);
+        }
+        $args{'ContentType'} = 'storable';
+    }
+
+    my $old_value = RT->Config->Get($args{Name});
+    unless (defined($old_value) && length($old_value)) {
+        $old_value = $self->loc('(no value)');
+    }
+
+    ( $id, $msg ) = $self->SUPER::Create(
+        map { $_ => $args{$_} } grep {exists $args{$_}}
+            qw(Name Content ContentType),
+    );
+    unless ($id) {
+        return (0, $self->loc("Setting [_1] to [_2] failed: [_3]", $args{Name}, $args{Content}, $msg));
+    }
+
+    RT->Config->ApplyConfigChangeToAllServerProcesses;
+
+    my ($content, $error) = $self->Content;
+    unless (defined($content) && length($content)) {
+        $content = $self->loc('(no value)');
+    }
+
+    if (!ref($content) && !ref($old_value)) {
+        RT->Logger->info($self->CurrentUser->Name . " changed " . $self->Name . " from " . $old_value . " to " . $content);
+        return ($id, $self->loc("[_1] changed from [_2] to [_3]", $self->Name, $old_value, $content));
+    }
+    else {
+        RT->Logger->info($self->CurrentUser->Name . " changed " . $self->Name);
+        return ($id, $self->loc("[_1] changed", $self->Name));
+    }
+}
+
+=head2 CurrentUserCanSee
+
+Returns true if the current user can see the database setting
+
+=cut
+
+sub CurrentUserCanSee {
+    my $self = shift;
+
+    return $self->CurrentUserHasRight('SuperUser');
+}
+
+=head2 Load
+
+Load a setting from the database. Takes a single argument. If the
+argument is numerical, load by the column 'id'. Otherwise, load by the
+"Name" column.
+
+=cut
+
+sub Load {
+    my $self = shift;
+    my $identifier = shift || return undef;
+
+    if ( $identifier !~ /\D/ ) {
+        return $self->SUPER::LoadById( $identifier );
+    } else {
+        return $self->LoadByCol( "Name", $identifier );
+    }
+}
+
+=head2 SetName
+
+Not permitted
+
+=cut
+
+sub SetName {
+    my $self = shift;
+    return (0, $self->loc("Permission Denied"));
+}
+
+=head2 ValidateName
+
+Returns either (0, "failure reason") or 1 depending on whether the given
+name is valid.
+
+=cut
+
+sub ValidateName {
+    my $self = shift;
+    my $name = shift;
+
+    return ( 0, $self->loc('empty name') ) unless defined $name && length $name;
+
+    my $TempSetting = RT::DatabaseSetting->new( RT->SystemUser );
+    $TempSetting->Load($name);
+
+    if ( $TempSetting->id && ( !$self->id || $TempSetting->id != $self->id ) ) {
+        return ( 0, $self->loc('Name in use') );
+    }
+    else {
+        return 1;
+    }
+}
+
+=head2 Delete
+
+Checks ACL, and on success propagates this config change to all server
+processes.
+
+=cut
+
+sub Delete {
+    my $self = shift;
+    return (0, $self->loc("Permission Denied")) unless $self->CurrentUserCanSee;
+    my ($ok, $msg) = $self->SUPER::Delete(@_);
+    return ($ok, $msg) if !$ok;
+    RT->Config->ApplyConfigChangeToAllServerProcesses;
+    RT->Logger->info($self->CurrentUser->Name . " removed database setting for " . $self->Name);
+    return ($ok, $self->loc("Database setting removed."));
+}
+
+=head2 DecodedContent
+
+Returns a pair of this setting's content and any error.
+
+=cut
+
+sub DecodedContent {
+    my $self = shift;
+
+    # Here we call _Value to run the ACL check.
+    my $content = $self->_Value('Content');
+
+    my $type = $self->__Value('ContentType') || '';
+
+    if ($type eq 'storable') {
+        return $self->_DeserializeContent($content);
+    }
+    elsif ($type eq 'application/json') {
+        return $self->_DeJSONContent($content);
+    }
+
+    return ($content, "");
+}
+
+=head2 SetContent
+
+=cut
+
+sub SetContent {
+    my $self         = shift;
+    my $value        = shift;
+    my $content_type = shift || '';
+
+    return (0, $self->loc("Permission Denied")) unless $self->CurrentUserCanSee;
+
+    my ($old_value, $error) = $self->Content;
+    unless (defined($old_value) && length($old_value)) {
+        $old_value = $self->loc('(no value)');
+    }
+
+    if (ref $value) {
+        ($value, my $error) = $self->_SerializeContent($value);
+        if ($error) {
+            return (0, $error);
+        }
+        $content_type = 'storable';
+    }
+
+    $RT::Handle->BeginTransaction;
+
+    my ($ok, $msg) = $self->_Set( Field => 'Content', Value => $value );
+    if (!$ok) {
+        $RT::Handle->Rollback;
+        return ($ok, $self->loc("Unable to update [_1]: [_2]", $self->Name, $msg));
+    }
+
+    if ($self->ContentType ne $content_type) {
+        ($ok, $msg) = $self->_Set( Field => 'ContentType', Value => $content_type );
+        if (!$ok) {
+            $RT::Handle->Rollback;
+            return ($ok, $self->loc("Unable to update [_1]: [_2]", $self->Name, $msg));
+        }
+    }
+
+    $RT::Handle->Commit;
+    RT->Config->ApplyConfigChangeToAllServerProcesses;
+
+    unless (defined($value) && length($value)) {
+        $value = $self->loc('(no value)');
+    }
+
+    if (!ref($value) && !ref($old_value)) {
+        RT->Logger->info($self->CurrentUser->Name . " changed " . $self->Name . " from " . $old_value . " to " . $value);
+        return ($ok, $self->loc("[_1] changed from [_2] to [_3]", $self->Name, $old_value, $value));
+    } else {
+        RT->Logger->info($self->CurrentUser->Name . " changed " . $self->Name);
+        return ($ok, $self->loc("[_1] changed", $self->Name));
+    }
+}
+
+=head1 PRIVATE METHODS
+
+Documented for internal use only, do not call these from outside
+RT::DatabaseSetting itself.
+
+=head2 _Set
+
+Checks if the current user has I<SuperUser> before calling
+C<SUPER::_Set>, and then propagates this config change to all server processes.
+
+=cut
+
+sub _Set {
+    my $self = shift;
+    my %args = (
+        Field => undef,
+        Value => undef,
+        @_
+    );
+
+    return (0, $self->loc("Permission Denied"))
+        unless $self->CurrentUserCanSee;
+
+    my ($ok, $msg) = $self->SUPER::_Set(@_);
+    RT->Config->ApplyConfigChangeToAllServerProcesses;
+    return ($ok, $msg);
+}
+
+=head2 _Value
+
+Checks L</CurrentUserCanSee> before calling C<SUPER::_Value>.
+
+=cut
+
+sub _Value {
+    my $self = shift;
+    return unless $self->CurrentUserCanSee;
+    return $self->SUPER::_Value(@_);
+}
+
+sub _SerializeContent {
+    my $self = shift;
+    my $content = shift;
+    my $name = shift || $self->Name;
+    my $frozen = eval { encode_base64(Storable::nfreeze($content)) };
+
+    if (my $error = $@) {
+        $RT::Logger->error("Storable serialization of database setting $name failed: $error");
+        return (undef, $self->loc("Storable serialization of database setting [_1] failed: [_2]", $name, $error));
+    }
+
+    return $frozen;
+}
+
+sub _DeserializeContent {
+    my $self = shift;
+    my $content = shift;
+
+    my $thawed = eval { Storable::thaw(decode_base64($content)) };
+    if (my $error = $@) {
+        $RT::Logger->error("Storable deserialization of database setting " . $self->Name . " failed: $error");
+        return (undef, $self->loc("Storable deserialization of database setting [_1] failed: [_2]", $self->Name, $error));
+    }
+
+    return $thawed;
+}
+
+sub _DeJSONContent {
+    my $self = shift;
+    my $content = shift;
+
+    my $thawed = eval { JSON::from_json($content) };
+    if (my $error = $@) {
+        $RT::Logger->error("JSON deserialization of database setting " . $self->Name . " failed: $error");
+        return (undef, $self->loc("JSON deserialization of database setting [_1] failed: [_2]", $self->Name, $error));
+    }
+
+    return $thawed;
+}
+
+sub Table { "DatabaseSettings" }
+
+sub _CoreAccessible {
+    {
+        id            => { read => 1, type => 'int(11)',        default => '' },
+        Name          => { read => 1, sql_type => 12, length => 255,  is_blob => 0,  is_numeric => 0,  type => 'varchar(255)', default => ''},
+        Content       => { read => 1, write => 1, sql_type => -4, length => 0,  is_blob => 1,  is_numeric => 0,  type => 'blob', default => ''},
+        ContentType   => { read => 1, write => 1, sql_type => 12, length => 16,  is_blob => 0,  is_numeric => 0,  type => 'varchar(16)', default => ''},
+        Disabled      => { read => 1, write => 1, sql_type => 5, length => 6,  is_blob => 0,  is_numeric => 1,  type => 'smallint(6)', default => '0'},
+        Creator       => { read => 1, type => 'int(11)',        default => '0', auto => 1 },
+        Created       => { read => 1, type => 'datetime',       default => '',  auto => 1 },
+        LastUpdatedBy => { read => 1, type => 'int(11)',        default => '0', auto => 1 },
+        LastUpdated   => { read => 1, type => 'datetime',       default => '',  auto => 1 },
+    }
+}
+
+1;
+
diff --git a/lib/RT/DatabaseSettings.pm b/lib/RT/DatabaseSettings.pm
new file mode 100644
index 0000000000..3236bad2c0
--- /dev/null
+++ b/lib/RT/DatabaseSettings.pm
@@ -0,0 +1,84 @@
+# BEGIN BPS TAGGED BLOCK {{{
+#
+# COPYRIGHT:
+#
+# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
+#                                          <sales at bestpractical.com>
+#
+# (Except where explicitly superseded by other copyright notices)
+#
+#
+# LICENSE:
+#
+# This work is made available to you under the terms of Version 2 of
+# the GNU General Public License. A copy of that license should have
+# been provided with this software, but in any event can be snarfed
+# from www.gnu.org.
+#
+# This work is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301 or visit their web page on the internet at
+# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+#
+#
+# CONTRIBUTION SUBMISSION POLICY:
+#
+# (The following paragraph is not intended to limit the rights granted
+# to you to modify and distribute this software under the terms of
+# the GNU General Public License and is only of importance to you if
+# you choose to contribute your changes and enhancements to the
+# community by submitting them to Best Practical Solutions, LLC.)
+#
+# By intentionally submitting any modifications, corrections or
+# derivatives to this work, or any other work intended for use with
+# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+# you are the copyright holder for those contributions and you grant
+# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+# royalty-free, perpetual, license to use, copy, create derivative
+# works based on those contributions, and sublicense and distribute
+# those contributions and any derivatives thereof.
+#
+# END BPS TAGGED BLOCK }}}
+
+use strict;
+use warnings;
+
+package RT::DatabaseSettings;
+use base 'RT::SearchBuilder';
+
+=head1 NAME
+
+RT::DatabaseSettings - a collection of L<RT::DatabaseSettings> objects
+
+=cut
+
+sub NewItem {
+    my $self = shift;
+    return RT::DatabaseSetting->new( $self->CurrentUser );
+}
+
+=head2 _Init
+
+Sets default ordering by id ascending.
+
+=cut
+
+sub _Init {
+    my $self = shift;
+
+    $self->{'with_disabled_column'} = 1;
+
+    $self->OrderBy( FIELD => 'id', ORDER => 'ASC' );
+    return $self->SUPER::_Init( @_ );
+}
+
+sub Table { "DatabaseSettings" }
+
+1;
+

commit 04e7c94870a1f51ecdaae33732e4a44943a623dd
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Aug 15 19:15:16 2017 +0000

    Port database config loading and refreshing from extension

diff --git a/lib/RT.pm b/lib/RT.pm
index 52fa299189..c53cbb5e38 100644
--- a/lib/RT.pm
+++ b/lib/RT.pm
@@ -199,6 +199,7 @@ sub Init {
     ConnectToDatabase();
     InitSystemObjects();
     InitClasses(%args);
+    RT->Config->LoadConfigFromDatabase();
     InitLogging();
     ProcessPreInitMessages();
     InitPlugins();
@@ -503,6 +504,8 @@ sub InitClasses {
     require RT::Asset;
     require RT::Assets;
     require RT::CustomFieldValues::Canonicalizer;
+    require RT::DatabaseSetting;
+    require RT::DatabaseSettings;
 
     _BuildTableAttributes();
 
diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index ab42501fdf..4150e5f252 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -55,6 +55,7 @@ use 5.010;
 use File::Spec ();
 use Symbol::Global::Name;
 use List::MoreUtils 'uniq';
+use Storable;
 
 # Store log messages generated before RT::Logger is available
 our @PreInitLoggerMessages;
@@ -1859,6 +1860,125 @@ sub EnableExternalAuth {
     return;
 }
 
+my $database_config_cache_time = 0;
+my %original_setting_from_files;
+my $in_config_change_txn = 0;
+
+sub BeginDatabaseConfigChanges {
+    $in_config_change_txn = $in_config_change_txn + 1;
+}
+
+sub EndDatabaseConfigChanges {
+    $in_config_change_txn = $in_config_change_txn - 1;
+    if (!$in_config_change_txn) {
+        shift->ApplyConfigChangeToAllServerProcesses();
+    }
+}
+
+sub ApplyConfigChangeToAllServerProcesses {
+    my $self = shift;
+
+    return if $in_config_change_txn;
+
+    # first apply locally
+    $self->LoadConfigFromDatabase();
+
+    # then notify other servers
+    RT->System->ConfigCacheNeedsUpdate($database_config_cache_time);
+}
+
+sub RefreshConfigFromDatabase {
+    my $self = shift;
+    if ($in_config_change_txn) {
+        RT->Logger->error("It appears that there were unbalanced calls to BeginDatabaseConfigChanges with EndDatabaseConfigChanges; this indicates a software fault");
+        $in_config_change_txn = 0;
+    }
+
+    my $needs_update = RT->System->ConfigCacheNeedsUpdate;
+    if ($needs_update > $database_config_cache_time) {
+        $self->LoadConfigFromDatabase();
+        $database_config_cache_time = $needs_update;
+    }
+}
+
+sub LoadConfigFromDatabase {
+    my $self = shift;
+
+    $database_config_cache_time = time;
+
+    my $settings = RT::DatabaseSettings->new(RT->SystemUser);
+    $settings->UnLimit;
+
+    my %seen;
+
+    while (my $setting = $settings->Next) {
+        my $name = $setting->Name;
+        my ($value, $error) = $setting->DecodedContent;
+        next if $error;
+
+        if (!exists $original_setting_from_files{$name}) {
+            $original_setting_from_files{$name} = [
+                scalar($self->Get($name)),
+                Storable::dclone(scalar($self->Meta($name))),
+            ];
+        }
+
+        $seen{$name}++;
+
+        # are we inadvertantly overriding RT_SiteConfig.pm?
+        my $meta = $META{$name};
+        if ($meta->{'Source'}) {
+            my %source = %{ $meta->{'Source'} };
+            if ($source{'SiteConfig'} && $source{'File'} ne 'database') {
+                RT->Logger->warning("Change of config option '$name' at $source{File} line $source{Line} has been overridden by the config setting from the database. Please remove it from $source{File} or from the database to avoid confusion.");
+            }
+        }
+
+        my $type = $meta->{Type} || 'SCALAR';
+
+        # hashes combine, but we don't want that behavior because the previous
+        # config settings will shadow any change that the database config makes
+        if ($type eq 'HASH') {
+            $self->Set($name, ());
+        }
+
+        my $val = $type eq 'ARRAY' ? $value
+                : $type eq 'HASH'  ? [ %$value ]
+                                   : [ $value ];
+
+        $self->SetFromConfig(
+            Option     => \$name,
+            Value      => $val,
+            Package    => 'N/A',
+            File       => 'database',
+            Line       => 'N/A',
+            SiteConfig => 1,
+        );
+    }
+
+    # anything that wasn't loaded from the database but has been set in
+    # %original_setting_from_files must have been disabled from the database,
+    # so we want to restore the original setting
+    for my $name (keys %original_setting_from_files) {
+        next if $seen{$name};
+
+        my ($value, $meta) = @{ $original_setting_from_files{$name} };
+        my $type = $meta->{Type} || 'SCALAR';
+
+        if ($type eq 'ARRAY') {
+            $self->Set($name, @$value);
+        }
+        elsif ($type eq 'HASH') {
+            $self->Set($name, %$value);
+        }
+        else {
+            $self->Set($name, $value);
+        }
+
+        %{ $META{$name} } = %$meta;
+    }
+}
+
 RT::Base->_ImportOverlays();
 
 1;
diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index 33380fe73b..d9e1501a5e 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -272,6 +272,8 @@ sub HandleRequest {
         Module::Refresh->refresh;
     }
 
+    RT->Config->RefreshConfigFromDatabase();
+
     $HTML::Mason::Commands::r->content_type("text/html; charset=utf-8");
 
     $HTML::Mason::Commands::m->{'rt_base_time'} = [ Time::HiRes::gettimeofday() ];
diff --git a/lib/RT/System.pm b/lib/RT/System.pm
index 7670b91fb3..e7b761eda1 100644
--- a/lib/RT/System.pm
+++ b/lib/RT/System.pm
@@ -236,6 +236,27 @@ sub CustomRoleCacheNeedsUpdate {
     }
 }
 
+=head2 ConfigCacheNeedsUpdate ( 1 )
+
+Attribute to decide when we need to flush the database settings
+and re-register any changes.  Set when settings are created, enabled/disabled, etc.
+
+If passed a true value, will update the attribute to be the current time.
+
+=cut
+
+sub ConfigCacheNeedsUpdate {
+    my $self = shift;
+    my $time = shift;
+
+    if ($time) {
+        return $self->SetAttribute(Name => 'ConfigCacheNeedsUpdate', Content => $time);
+    } else {
+        my $cache = $self->FirstAttribute('ConfigCacheNeedsUpdate');
+        return (defined $cache ? $cache->Content : 0 );
+    }
+}
+
 =head2 AddUpgradeHistory package, data
 
 Adds an entry to the upgrade history database. The package can be either C<RT>

commit 55a4dac8c09c7f51157537362ffe1ba54a222363
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Aug 15 19:17:05 2017 +0000

    Add Code and MultilineString widgets

diff --git a/share/html/Widgets/Form/Code b/share/html/Widgets/Form/Code
new file mode 100644
index 0000000000..1c346d6c18
--- /dev/null
+++ b/share/html/Widgets/Form/Code
@@ -0,0 +1,57 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%DOC>
+see docs/extending/using_forms_widgets.pod
+</%DOC>
+<& /Widgets/Form/MultilineString, Class => 'code', %ARGS &>
+<%METHOD InputOnly>
+<& /Widgets/Form/MultilineString:InputOnly, %ARGS &>
+</%METHOD>
+<%METHOD Process>
+<& /Widgets/Form/MultilineString:Process, %ARGS &>
+</%METHOD>
diff --git a/share/html/Widgets/Form/MultilineString b/share/html/Widgets/Form/MultilineString
new file mode 100644
index 0000000000..fa6a74758d
--- /dev/null
+++ b/share/html/Widgets/Form/MultilineString
@@ -0,0 +1,102 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%DOC>
+see docs/extending/using_forms_widgets.pod
+</%DOC>
+<div id="form-box-<% lc $Name %>" class="widget <% $Class %>">
+<span class="description label"><% $Description %></span>
+<span class="value"><& SELF:InputOnly, %ARGS &></span>
+% if ( $Default ) {
+<span class="comment"><% $DefaultLabel %></span>
+% }
+<span class="hints"><% $Hints %></span>
+</div>
+<%ARGS>
+$Name
+
+$Class        => ''
+$Description  => undef,
+$Hints        => ''
+
+$CurrentValue => '',
+
+$Default        => 0,
+$DefaultValue   => '',
+$DefaultLabel   => loc( 'Default: [_1]', $DefaultValue ),
+</%ARGS>
+
+<%METHOD InputOnly>
+<textarea name="<% $Name %>" cols="<% $Cols %>" rows="<% $Rows %>"><% $CurrentValue %></textarea>
+<%ARGS>
+$Name
+$Cols => 80
+$Rows => 6
+$CurrentValue => '',
+</%ARGS>
+</%METHOD>
+
+<%METHOD Process>
+<%ARGS>
+$Name
+
+$Arguments    => {},
+
+$Default      => 0,
+$DefaultValue => '',
+</%ARGS>
+<%INIT>
+my $value = $Arguments->{ $Name };
+$value = '' unless defined $value;
+
+if ( $value eq '' ) {
+    return $DefaultValue unless $Default;
+    return undef;
+}
+return $value;
+</%INIT>
+</%METHOD>

commit d967bbbc93b862f56dd966d11f581be8b013f771
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Aug 15 19:23:14 2017 +0000

    Port EditConfig page from extension

diff --git a/lib/RT/Interface/Web/MenuBuilder.pm b/lib/RT/Interface/Web/MenuBuilder.pm
index 809c32cd8f..9834786d2b 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -981,6 +981,11 @@ sub _BuildAdminMenu {
         description => loc('Detailed information about your RT setup'),
         path        => '/Admin/Tools/Configuration.html',
     );
+    $admin_tools->child( edit_config =>
+        title       => loc('Edit Configuration'),
+        description => loc('Update your RT setup'),
+        path        => '/Admin/Tools/EditConfig.html',
+    );
     $admin_tools->child( theme =>
         title       => loc('Theme'),
         description => loc('Customize the look of your RT'),
diff --git a/share/html/Admin/Tools/EditConfig.html b/share/html/Admin/Tools/EditConfig.html
new file mode 100644
index 0000000000..c3185edf47
--- /dev/null
+++ b/share/html/Admin/Tools/EditConfig.html
@@ -0,0 +1,270 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+<%INIT>
+my $title = loc('System Configuration');
+unless ($session{'CurrentUser'}->HasRight( Object=> $RT::System, Right => 'SuperUser')) {
+ Abort(loc('This feature is only available to system administrators'));
+}
+
+my $has_execute_code = $session{CurrentUser}->HasRight(Right => 'ExecuteCode', Object => RT->System);
+
+my @results;
+
+my $doc_version = $RT::VERSION;
+$doc_version =~ s/rc\d+//; # 4.4.2rc1 -> 4.4.2
+$doc_version =~ s/\.\d+-\d+-g\w+$//;  # 4.4.3-1-g123 -> 4.4
+
+use Data::Dumper;
+my $stringify = sub {
+    my $value = shift;
+    return "" if !defined($value);
+
+    local $Data::Dumper::Terse = 1;
+    local $Data::Dumper::Indent = 2;
+    local $Data::Dumper::Sortkeys = 1;
+    my $output = Dumper $value;
+    chomp $output;
+    return $output;
+};
+
+if (delete $ARGS{Update}) {
+    RT->Config->BeginDatabaseConfigChanges;
+    $RT::Handle->BeginTransaction;
+    my $has_error;
+
+    eval {
+        for my $key (keys %ARGS) {
+            next if $key =~ /-Current$/;
+
+            my $meta = RT->Config->Meta( $key );
+            my $widget = $meta->{Widget} || '/Widgets/Form/Code';
+            my $is_code = $widget eq '/Widgets/Form/Code';
+
+            my $val = $ARGS{$key};
+            $val = '' if $val eq '__empty_value__';
+            my $prev = $ARGS{$key . '-Current'};
+            next if $val eq $prev;
+
+            # for bools, check for truthiness since 0, '', and undef are equivalent
+            if ($widget eq '/Widgets/Form/Boolean') {
+                next if !!$val eq !!$prev;
+            }
+
+            if ( $meta->{Immutable} || $meta->{Obfuscate} || ($key =~ /Password/i and $key !~ /MinimumPasswordLength|AllowLoginPasswordAutoComplete/ )) {
+                push @results, loc("Cannot change [_1]: Permission Denied", $key);
+                $has_error++;
+                next;
+            }
+
+            if ($is_code) {
+                if (!$has_execute_code) {
+                    push @results, loc("Cannot change [_1]: Permission Denied", $key);
+                    $has_error++;
+                    next;
+                }
+
+                my $code = $val;
+                my $coderef;
+                # similar to RT::Scrip::CompileCheck
+                do {
+                    no strict 'vars';
+                    $coderef = eval "sub { $code \n }";
+                };
+                if ($@) {
+                    my $error = $@;
+                    push @results, loc("Couldn't compile [_1] codeblock '[_2]': [_3]", $key, $code, $error);
+                    $has_error++;
+                    next;
+                }
+
+                if ($coderef) {
+                    $val = eval { $coderef->() };
+                    if ($@) {
+                        my $error = $@;
+                        push @results, loc("Couldn't execute [_1] codeblock '[_2]': [_3]", $key, $code, $error);
+                        $has_error++;
+                        next;
+                    }
+                }
+            }
+
+            my $setting = RT::DatabaseSetting->new($session{CurrentUser});
+            $setting->Load($key);
+            if ($setting->Id) {
+                if ($setting->Disabled) {
+                    $setting->SetDisabled(0);
+                }
+
+                my ($ok, $msg) = $setting->SetContent($val);
+                push @results, $msg;
+                $has_error++ if !$ok;
+            }
+            else {
+                my ($ok, $msg) = $setting->Create(
+                    Name    => $key,
+                    Content => $val,
+                );
+                push @results, $msg;
+                $has_error++ if !$ok;
+            }
+        }
+    };
+
+    if ($@) {
+        push @results, $@;
+        $has_error++;
+    }
+
+    if ($has_error) {
+        push @results, loc("No changes made.");
+        $RT::Handle->Rollback;
+    }
+    else {
+        $RT::Handle->Commit;
+    }
+    RT->Config->EndDatabaseConfigChanges;
+}
+
+</%INIT>
+<& /Admin/Elements/Header, Title => $title &>
+<& /Elements/Tabs &>
+<& /Elements/ListActions, actions => \@results &>
+
+<form id="EditConfig" method="post" action="EditConfig.html">
+<input type="hidden" name="Update" value=1></input>
+
+<&|/Widgets/TitleBox, title => loc("RT Configuration") &>
+<table border="0" cellspacing="0" cellpadding="5" width="100%" class="collection">
+<tr class="collection-as-table">
+<th class="collection-as-table"><&|/l&>Option</&></th>
+<th class="collection-as-table"><&|/l&>Value</&></th>
+</tr>
+<%PERL>
+my $index_conf;
+foreach my $key ( RT->Config->Options( Overridable => undef, Sorted => 0 ) ) {
+    my $meta = RT->Config->Meta( $key );
+
+    next if $meta->{Invisible};
+
+    my $raw_value = RT->Config->Get( $key );
+    my $val = $stringify->($raw_value);
+
+    $index_conf++;
+
+    my $doc_url = "https://docs.bestpractical.com/rt/$doc_version/RT_Config.html#$key";
+
+    my $widget = $meta->{'Widget'} || '/Widgets/Form/Code';
+    my $is_code = $widget eq '/Widgets/Form/Code';
+    my $is_password = ($key =~ /Password/i and $key !~ /MinimumPasswordLength|AllowLoginPasswordAutoComplete/ );
+    my $is_immutable = $meta->{Immutable}
+                    || $meta->{Obfuscate}
+                    || ($is_code && $val =~ s/sub { "DUMMY" }/sub { ... }/g)
+                    || ($is_code && !$has_execute_code);
+
+    my $current_value = $is_code ? $val : $raw_value;
+    my $args   = $meta->{'WidgetArguments'} || {};
+
+    if ($widget eq '/Widgets/Form/Boolean') {
+        %$args = (
+            Default => 0,
+            RadioStyle => 1,
+            %$args,
+        );
+    }
+    elsif ($widget eq '/Widgets/Form/String' || $widget eq '/Widgets/Form/Integer') {
+        %$args = (
+            Size => 60,
+            %$args,
+        );
+    }
+
+</%PERL>
+<tr class="<% $key %> <% $index_conf%2 ? 'oddline' : 'evenline'%>">
+<td class="collection-as-table"><a href="<% $doc_url %>" target="_blank"><% $key %></a></td>
+<td class="collection-as-table">
+
+% if ( $meta->{EditLink} ) {
+<&|/l_unsafe, "<a href=\"$meta->{EditLink}\">", loc($meta->{EditLinkLabel}), "</a>" &>Visit [_1][_2][_3] to manage this setting</&>
+% } elsif ( $key =~ /Plugins/) {
+<ul class="plugins">
+% for my $plugin (RT->Config->Get($key)) {
+<li><a href="https://metacpan.org/search?q=<% $plugin |u %>" target="_blank"><% $plugin %></a></li>
+% }
+</ul>
+<br><em><% loc('Must modify in config file' ) %></em>
+% } elsif ( $is_password ) {
+<em><% loc('Must modify in config file' ) %></em>
+% } elsif ( $is_immutable ) {
+% if ($widget eq '/Widgets/Form/MultilineString' || $widget eq '/Widgets/Form/Code') {
+<textarea disabled class="<% $is_code ? 'code' : '' %>" rows="6" cols="80"><% $current_value %></textarea>
+% } else {
+<input type="text" disabled width="80" value="<% $current_value %>"></input>
+% }
+<br><em><% loc('Must modify in config file' ) %></em>
+% } else {
+  <& $widget,
+    Default      => 1,
+    DefaultValue => '',
+    DefaultLabel => '(no value)',
+
+    %{ $m->comp('/Widgets/FinalizeWidgetArguments', WidgetArguments => $args ) },
+    Name         => $key,
+    CurrentValue => $current_value,
+    Description  => '',
+    Hints        => '',
+  &>
+<textarea class="hidden" name="<% $key %>-Current"><% $current_value %></textarea>
+% }
+</td>
+</tr>
+% }
+</table>
+</&>
+<& /Elements/Submit, Label => loc('Save Changes') &>
+</form>
+

commit 9f34fb6d5c398eb73ebd61440a6d5fd787065ad8
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Aug 15 19:33:14 2017 +0000

    Annotate Immutable options
    
    These options cannot be changed in database settings

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 4150e5f252..e0c3bc1ad4 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -638,6 +638,7 @@ our %META;
     # this tends to break extensions that stash links in ticket update pages
     Organization => {
         Type            => 'SCALAR',
+        Immutable       => 1,
         PostLoadCheck   => sub {
             my ($self,$value) = @_;
             $RT::Logger->error("your \$Organization setting ($value) appears to contain whitespace.  Please fix this.")
@@ -645,9 +646,38 @@ our %META;
         },
     },
 
+    rtname => {
+        Immutable => 1,
+    },
+
     # Internal config options
     DatabaseExtraDSN => {
-        Type => 'HASH',
+        Type      => 'HASH',
+        Immutable => 1,
+    },
+    DatabaseAdmin => {
+        Immutable => 1,
+    },
+    DatabaseHost => {
+        Immutable => 1,
+    },
+    DatabaseName => {
+        Immutable => 1,
+    },
+    DatabasePassword => {
+        Immutable => 1,
+    },
+    DatabasePort => {
+        Immutable => 1,
+    },
+    DatabaseRTHost => {
+        Immutable => 1,
+    },
+    DatabaseType => {
+        Immutable => 1,
+    },
+    DatabaseUser => {
+        Immutable => 1,
     },
 
     FullTextSearch => {
@@ -741,8 +771,24 @@ our %META;
         Type => 'SCALAR',
         PostLoadCheck => sub { RT::Interface::Email->_HTMLFormatter },
     },
+    Plugins => {
+        Immutable => 1,
+    },
+    RecordBaseClass => {
+        Immutable => 1,
+    },
+    WebSessionClass => {
+        Immutable => 1,
+    },
+    DevelMode => {
+        Immutable => 1,
+    },
+    DisallowExecuteCode => {
+        Immutable => 1,
+    },
     MailPlugins  => {
         Type => 'ARRAY',
+        Immutable     => 1,
         PostLoadCheck => sub {
             my $self = shift;
 
@@ -868,6 +914,7 @@ our %META;
     EmailDashboardLanguageOrder  => { Type => 'ARRAY' },
     CustomFieldValuesCanonicalizers => { Type => 'ARRAY' },
     WebPath => {
+        Immutable     => 1,
         PostLoadCheck => sub {
             my $self  = shift;
             my $value = shift;
@@ -894,6 +941,7 @@ our %META;
         },
     },
     WebDomain => {
+        Immutable => 1,
         PostLoadCheck => sub {
             my $self  = shift;
             my $value = shift;
@@ -920,6 +968,7 @@ our %META;
         },
     },
     WebPort => {
+        Immutable     => 1,
         PostLoadCheck => sub {
             my $self  = shift;
             my $value = shift;
@@ -935,6 +984,7 @@ our %META;
         },
     },
     WebBaseURL => {
+        Immutable     => 1,
         PostLoadCheck => sub {
             my $self  = shift;
             my $value = shift;
@@ -958,6 +1008,7 @@ our %META;
         },
     },
     WebURL => {
+        Immutable     => 1,
         PostLoadCheck => sub {
             my $self  = shift;
             my $value = shift;
@@ -1103,7 +1154,12 @@ our %META;
         },
     },
 
+    ExternalAuth => {
+        Immutable => 1,
+    },
+
     ExternalSettings => {
+        Immutable     => 1,
         Obfuscate => sub {
             # Ensure passwords are obfuscated on the System Configuration page
             my ($config, $sources, $user) = @_;
@@ -1167,6 +1223,7 @@ our %META;
     },
 
     ExternalAuthPriority => {
+        Immutable     => 1,
         PostLoadCheck => sub {
             my $self = shift;
             my @values = @{ shift || [] };
@@ -1195,6 +1252,7 @@ our %META;
     },
 
     ExternalInfoPriority => {
+        Immutable     => 1,
         PostLoadCheck => sub {
             my $self = shift;
             my @values = @{ shift || [] };

commit a85c0c08b3cfbf442322c8d9018f44fc24a452e8
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Aug 15 19:40:29 2017 +0000

    Add widget metadata for config options

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index e0c3bc1ad4..faa2165e3f 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -639,6 +639,7 @@ our %META;
     Organization => {
         Type            => 'SCALAR',
         Immutable       => 1,
+        Widget          => '/Widgets/Form/String',
         PostLoadCheck   => sub {
             my ($self,$value) = @_;
             $RT::Logger->error("your \$Organization setting ($value) appears to contain whitespace.  Please fix this.")
@@ -648,6 +649,7 @@ our %META;
 
     rtname => {
         Immutable => 1,
+        Widget    => '/Widgets/Form/String',
     },
 
     # Internal config options
@@ -657,27 +659,35 @@ our %META;
     },
     DatabaseAdmin => {
         Immutable => 1,
+        Widget    => '/Widgets/Form/String',
     },
     DatabaseHost => {
         Immutable => 1,
+        Widget    => '/Widgets/Form/String',
     },
     DatabaseName => {
         Immutable => 1,
+        Widget    => '/Widgets/Form/String',
     },
     DatabasePassword => {
         Immutable => 1,
+        Widget    => '/Widgets/Form/String',
     },
     DatabasePort => {
         Immutable => 1,
+        Widget    => '/Widgets/Form/Integer',
     },
     DatabaseRTHost => {
         Immutable => 1,
+        Widget    => '/Widgets/Form/String',
     },
     DatabaseType => {
         Immutable => 1,
+        Widget    => '/Widgets/Form/String',
     },
     DatabaseUser => {
         Immutable => 1,
+        Widget    => '/Widgets/Form/String',
     },
 
     FullTextSearch => {
@@ -736,6 +746,7 @@ our %META;
     },
     DisableGraphViz => {
         Type            => 'SCALAR',
+        Widget          => '/Widgets/Form/Boolean',
         PostLoadCheck   => sub {
             my $self  = shift;
             my $value = shift;
@@ -747,6 +758,7 @@ our %META;
     },
     DisableGD => {
         Type            => 'SCALAR',
+        Widget          => '/Widgets/Form/Boolean',
         PostLoadCheck   => sub {
             my $self  = shift;
             my $value = shift;
@@ -758,6 +770,7 @@ our %META;
     },
     MailCommand => {
         Type    => 'SCALAR',
+        Widget  => '/Widgets/Form/String',
         PostLoadCheck => sub {
             my $self = shift;
             my $value = $self->Get('MailCommand');
@@ -769,6 +782,7 @@ our %META;
     },
     HTMLFormatter => {
         Type => 'SCALAR',
+        Widget => '/Widgets/Form/String',
         PostLoadCheck => sub { RT::Interface::Email->_HTMLFormatter },
     },
     Plugins => {
@@ -776,15 +790,19 @@ our %META;
     },
     RecordBaseClass => {
         Immutable => 1,
+        Widget    => '/Widgets/Form/String',
     },
     WebSessionClass => {
         Immutable => 1,
+        Widget    => '/Widgets/Form/String',
     },
     DevelMode => {
         Immutable => 1,
+        Widget    => '/Widgets/Form/Boolean',
     },
     DisallowExecuteCode => {
         Immutable => 1,
+        Widget    => '/Widgets/Form/Boolean',
     },
     MailPlugins  => {
         Type => 'ARRAY',
@@ -915,6 +933,7 @@ our %META;
     CustomFieldValuesCanonicalizers => { Type => 'ARRAY' },
     WebPath => {
         Immutable     => 1,
+        Widget        => '/Widgets/Form/String',
         PostLoadCheck => sub {
             my $self  = shift;
             my $value = shift;
@@ -941,7 +960,8 @@ our %META;
         },
     },
     WebDomain => {
-        Immutable => 1,
+        Immutable     => 1,
+        Widget        => '/Widgets/Form/String',
         PostLoadCheck => sub {
             my $self  = shift;
             my $value = shift;
@@ -969,6 +989,7 @@ our %META;
     },
     WebPort => {
         Immutable     => 1,
+        Widget        => '/Widgets/Form/Integer',
         PostLoadCheck => sub {
             my $self  = shift;
             my $value = shift;
@@ -985,6 +1006,7 @@ our %META;
     },
     WebBaseURL => {
         Immutable     => 1,
+        Widget        => '/Widgets/Form/String',
         PostLoadCheck => sub {
             my $self  = shift;
             my $value = shift;
@@ -1009,6 +1031,7 @@ our %META;
     },
     WebURL => {
         Immutable     => 1,
+        Widget => '/Widgets/Form/String',
         PostLoadCheck => sub {
             my $self  = shift;
             my $value = shift;
@@ -1156,6 +1179,7 @@ our %META;
 
     ExternalAuth => {
         Immutable => 1,
+        Widget    => '/Widgets/Form/Boolean',
     },
 
     ExternalSettings => {
@@ -1286,7 +1310,6 @@ our %META;
             $self->Set( 'ExternalInfoPriority', \@values );
         },
     },
-
     ServiceBusinessHours => {
         Type => 'HASH',
         PostLoadCheck   => sub {
@@ -1299,10 +1322,359 @@ our %META;
             }
         },
     },
-
     ServiceAgreements => {
         Type => 'HASH',
     },
+    AllowUserAutocompleteForUnprivileged => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    AlwaysDownloadAttachments => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    AmbiguousDayInFuture => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    AmbiguousDayInPast => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    ApprovalRejectionNotes => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    ArticleOnTicketCreate => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    AutocompleteOwnersForSearch => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    CanonicalizeRedirectURLs => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    CanonicalizeURLsInFeeds => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    ChartsTimezonesInDB => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    CheckMoreMSMailHeaders => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    DateDayBeforeMonth => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    DontSearchFileAttachments => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    DropLongAttachments => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    EditCustomFieldsSingleColumn => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    EnableReminders => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    ExternalStorageDirectLink => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    ForceApprovalsView => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    ForwardFromUser => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    Framebusting => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    HideArticleSearchOnReplyCreate => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    HideResolveActionsWithDependencies => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    HideTimeFieldsFromUnprivilegedUsers => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    LoopsToRTOwner => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    MessageBoxIncludeSignature => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    MessageBoxIncludeSignatureOnComment => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    OnlySearchActiveTicketsInSimpleSearch => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    ParseNewMessageForTicketCcs => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    PreferDateTimeFormatNatural => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    PreviewScripMessages => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    RecordOutgoingEmail => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    RestrictLoginReferrer => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    RestrictReferrer => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    SearchResultsAutoRedirect => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    ShowBccHeader => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    ShowMoreAboutPrivilegedUsers => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    ShowRTPortal => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    ShowRemoteImages => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    ShowTransactionImages => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    StoreLoops => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    StrictLinkACL => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    SuppressInlineTextFiles => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    TruncateLongAttachments => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    TrustHTMLAttachments => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    UseFriendlyFromLine => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    UseFriendlyToLine => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    UseOriginatorHeader => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    UseSQLForACLChecks => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    UseTransactionBatch => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    ValidateUserEmailAddresses => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    WebFallbackToRTLogin => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    WebFlushDbCacheEveryRequest => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    WebHttpOnlyCookies => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    WebRemoteUserAuth => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    WebRemoteUserAutocreate => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    WebRemoteUserContinuous => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    WebRemoteUserGecos => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    WebSecureCookies => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    WikiImplicitLinks => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    HideOneTimeSuggestions => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    LinkArticlesOnInclude => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    SelfServiceCorrespondenceOnly => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    ShowSearchResultCount => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+
+    AttachmentListCount => {
+        Widget => '/Widgets/Form/Integer',
+    },
+    AutoLogoff => {
+        Widget => '/Widgets/Form/Integer',
+    },
+    BcryptCost => {
+        Widget => '/Widgets/Form/Integer',
+    },
+    DefaultSummaryRows => {
+        Widget => '/Widgets/Form/Integer',
+    },
+    ExternalStorageCutoffSize => {
+        Widget => '/Widgets/Form/Integer',
+    },
+    LogoutRefresh => {
+        Widget => '/Widgets/Form/Integer',
+    },
+    MaxAttachmentSize => {
+        Widget => '/Widgets/Form/Integer',
+    },
+    MaxFulltextAttachmentSize => {
+        Widget => '/Widgets/Form/Integer',
+    },
+    MinimumPasswordLength => {
+        Widget => '/Widgets/Form/Integer',
+    },
+    MoreAboutRequestorGroupsLimit => {
+        Widget => '/Widgets/Form/Integer',
+    },
+    TicketsItemMapSize => {
+        Widget => '/Widgets/Form/Integer',
+    },
+
+    CommentAddress => {
+        Widget => '/Widgets/Form/String',
+    },
+    CorrespondAddress => {
+        Widget => '/Widgets/Form/String',
+    },
+    DashboardAddress => {
+        Widget => '/Widgets/Form/String',
+    },
+    DashboardSubject => {
+        Widget => '/Widgets/Form/String',
+    },
+    DefaultErrorMailPrecedence => {
+        Widget => '/Widgets/Form/String',
+    },
+    DefaultMailPrecedence => {
+        Widget => '/Widgets/Form/String',
+    },
+    DefaultSearchResultOrderBy => {
+        Widget => '/Widgets/Form/String',
+    },
+    EmailOutputEncoding => {
+        Widget => '/Widgets/Form/String',
+    },
+    FriendlyFromLineFormat => {
+        Widget => '/Widgets/Form/String',
+    },
+    FriendlyToLineFormat => {
+        Widget => '/Widgets/Form/String',
+    },
+    LogDir => {
+        Widget => '/Widgets/Form/String',
+    },
+    LogToFileNamed => {
+        Widget => '/Widgets/Form/String',
+    },
+    LogoAltText => {
+        Widget => '/Widgets/Form/String',
+    },
+    LogoLinkURL => {
+        Widget => '/Widgets/Form/String',
+    },
+    LogoURL => {
+        Widget => '/Widgets/Form/String',
+    },
+    OwnerEmail => {
+        Widget => '/Widgets/Form/String',
+    },
+    RedistributeAutoGeneratedMessages => {
+        Widget => '/Widgets/Form/String',
+    },
+    SendmailArguments => {
+        Widget => '/Widgets/Form/String',
+    },
+    SendmailBounceArguments => {
+        Widget => '/Widgets/Form/String',
+    },
+    SendmailPath => {
+        Widget => '/Widgets/Form/String',
+    },
+    SetOutgoingMailFrom => {
+        Widget => '/Widgets/Form/String',
+    },
+    Timezone => {
+        Widget => '/Widgets/Form/String',
+    },
+    WebImagesURL => {
+        Widget => '/Widgets/Form/String',
+    },
+
+    AssetSearchFormat => {
+        Widget => '/Widgets/Form/MultilineString',
+    },
+    AssetSummaryFormat => {
+        Widget => '/Widgets/Form/MultilineString',
+    },
+    AssetSummaryRelatedTicketsFormat => {
+        Widget => '/Widgets/Form/MultilineString',
+    },
+    DefaultSearchResultFormat => {
+        Widget => '/Widgets/Form/MultilineString',
+    },
+    DefaultSelfServiceSearchResultFormat => {
+        Widget => '/Widgets/Form/MultilineString',
+    },
+    MoreAboutRequestorExtraInfo => {
+        Widget => '/Widgets/Form/MultilineString',
+    },
+    MoreAboutRequestorTicketListFormat => {
+        Widget => '/Widgets/Form/MultilineString',
+    },
+    UserSearchResultFormat => {
+        Widget => '/Widgets/Form/MultilineString',
+    },
+    UserSummaryExtraInfo => {
+        Widget => '/Widgets/Form/MultilineString',
+    },
+    UserSummaryTicketListFormat => {
+        Widget => '/Widgets/Form/MultilineString',
+    },
+
+    LogToSyslog => {
+        Widget => '/Widgets/Form/Select',
+        WidgetArguments => { Values => [qw(debug info notice warning error critical alert emergency)] },
+    },
+    LogToSTDERR => {
+        Widget => '/Widgets/Form/Select',
+        WidgetArguments => { Values => [qw(debug info notice warning error critical alert emergency)] },
+    },
+    LogToFile => {
+        Widget => '/Widgets/Form/Select',
+        WidgetArguments => { Values => [qw(debug info notice warning error critical alert emergency)] },
+    },
+    LogStackTraces => {
+        Widget => '/Widgets/Form/Select',
+        WidgetArguments => { Values => [qw(debug info notice warning error critical alert emergency)] },
+    },
+    StatementLog => {
+        Widget => '/Widgets/Form/Select',
+        WidgetArguments => { Values => [qw(debug info notice warning error critical alert emergency)] },
+    },
+
+    DefaultSearchResultOrder => {
+        Widget => '/Widgets/Form/Select',
+        WidgetArguments => { Values => [qw(ASC DESC)] },
+    },
 );
 my %OPTIONS = ();
 my @LOADED_CONFIGS = ();

commit 6d61a93a18abeacb5c51b6c8df7cf5751366549e
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Aug 15 19:54:10 2017 +0000

    Hide deprecated options

diff --git a/share/html/Admin/Tools/EditConfig.html b/share/html/Admin/Tools/EditConfig.html
index c3185edf47..c606d5e0d8 100644
--- a/share/html/Admin/Tools/EditConfig.html
+++ b/share/html/Admin/Tools/EditConfig.html
@@ -189,7 +189,7 @@ my $index_conf;
 foreach my $key ( RT->Config->Options( Overridable => undef, Sorted => 0 ) ) {
     my $meta = RT->Config->Meta( $key );
 
-    next if $meta->{Invisible};
+    next if $meta->{Invisible} || $meta->{Deprecated};
 
     my $raw_value = RT->Config->Get( $key );
     my $val = $stringify->($raw_value);

commit 1e69c96f2ce70f6096ed874e94f3bc9d4945b2e0
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Aug 15 20:08:02 2017 +0000

    List Database as source of configuration on Sys Config page

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index faa2165e3f..27ae169d2c 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -2132,7 +2132,7 @@ sub SetFromConfig {
     }
 
     $META{$name}->{'Type'} = $type;
-    foreach (qw(Package File Line SiteConfig Extension)) {
+    foreach (qw(Package File Line SiteConfig Extension Database)) {
         $META{$name}->{'Source'}->{$_} = $args{$_};
     }
     $self->Set( $name, @{ $args{'Value'} } );
@@ -2382,6 +2382,7 @@ sub LoadConfigFromDatabase {
             Package    => 'N/A',
             File       => 'database',
             Line       => 'N/A',
+            Database   => 1,
             SiteConfig => 1,
         );
     }
diff --git a/share/html/Admin/Tools/Configuration.html b/share/html/Admin/Tools/Configuration.html
index baf0abf027..0f34470a5b 100644
--- a/share/html/Admin/Tools/Configuration.html
+++ b/share/html/Admin/Tools/Configuration.html
@@ -70,7 +70,10 @@ foreach my $key ( RT->Config->Options( Overridable => undef, Sorted => 0 ) ) {
 
     my $meta = RT->Config->Meta( $key );
     my $description = '';
-    if ( $meta->{'Source'}{'Extension'} && $meta->{'Source'}{'SiteConfig'} ) {
+    if ( $meta->{'Source'}{'Database'}) {
+        $description = loc("database");
+    }
+    elsif ( $meta->{'Source'}{'Extension'} && $meta->{'Source'}{'SiteConfig'} ) {
         $description = loc("[_1] site config", $meta->{'Source'}{'Extension'});
     }
     elsif ( $meta->{'Source'}{'Extension'} ) {

commit 0bf9dcf3019d1db3bcee263915d109a8d25a0864
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Aug 15 19:23:14 2017 +0000

    Port EditConfig page from extension

diff --git a/share/static/css/elevator-light/forms.css b/share/static/css/elevator-light/forms.css
index 7c2c122669..2554a2afa9 100644
--- a/share/static/css/elevator-light/forms.css
+++ b/share/static/css/elevator-light/forms.css
@@ -363,3 +363,23 @@ ul li .dropdown-item.active span,
 ul li .dropdown-item:active span {
   color: #fff;
 }
+
+/* remove unnecessary left padding for radio options */
+#EditConfig div.widget .label {
+    width: auto;
+    float: none;
+}
+
+#EditConfig textarea:disabled,
+#EditConfig input:disabled {
+    background-color: #EEE;
+}
+
+.widget.code textarea,
+textarea.code {
+    font-family: Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace;
+}
+
+#EditConfig ul.plugins {
+    margin: 0;
+}

commit b8b29e09caf6c683e0c994625b9866c9bac5f194
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Sep 5 14:56:57 2017 -0400

    Render config with EditLink as readonly
    
    That way you can still easily export the settings to another RT

diff --git a/share/html/Admin/Tools/EditConfig.html b/share/html/Admin/Tools/EditConfig.html
index c606d5e0d8..62adc78734 100644
--- a/share/html/Admin/Tools/EditConfig.html
+++ b/share/html/Admin/Tools/EditConfig.html
@@ -229,6 +229,11 @@ foreach my $key ( RT->Config->Options( Overridable => undef, Sorted => 0 ) ) {
 <td class="collection-as-table">
 
 % if ( $meta->{EditLink} ) {
+% if ($widget eq '/Widgets/Form/MultilineString' || $widget eq '/Widgets/Form/Code') {
+<textarea disabled class="<% $is_code ? 'code' : '' %>" rows="6" cols="80"><% $current_value %></textarea><br>
+% } else {
+<input type="text" disabled width="80" value="<% $current_value %>"></input><br>
+% }
 <&|/l_unsafe, "<a href=\"$meta->{EditLink}\">", loc($meta->{EditLinkLabel}), "</a>" &>Visit [_1][_2][_3] to manage this setting</&>
 % } elsif ( $key =~ /Plugins/) {
 <ul class="plugins">

commit d5c0a2580368859557189bc6f4511985bf8a22c8
Author: michel <michel at bestpractical.com>
Date:   Thu Oct 24 18:40:45 2019 +0200

    Switch from Storable::dclone to Clone::clone to handle code/regex

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 27ae169d2c..8d628aca7a 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -55,7 +55,7 @@ use 5.010;
 use File::Spec ();
 use Symbol::Global::Name;
 use List::MoreUtils 'uniq';
-use Storable;
+use Clone ();
 
 # Store log messages generated before RT::Logger is available
 our @PreInitLoggerMessages;
@@ -1994,7 +1994,6 @@ sub GetObfuscated {
 
     return $self->Get(@_) unless $obfuscate;
 
-    require Clone;
     my $res = Clone::clone( $self->Get( @_ ) );
     $res = $obfuscate->( $self, $res, $user );
     return $self->_ReturnValue( $res, $META{$name}->{'Type'} || 'SCALAR' );
@@ -2349,7 +2348,7 @@ sub LoadConfigFromDatabase {
         if (!exists $original_setting_from_files{$name}) {
             $original_setting_from_files{$name} = [
                 scalar($self->Get($name)),
-                Storable::dclone(scalar($self->Meta($name))),
+                Clone::clone(scalar($self->Meta($name))),
             ];
         }
 

commit 4e878f2e6a48a6c90de420a238bfdf9e377d03a4
Author: michel <michel at bestpractical.com>
Date:   Thu Oct 24 18:43:09 2019 +0200

    Fix index of "Disabled" for mysql

diff --git a/etc/schema.mysql b/etc/schema.mysql
index 058f858709..f37469e89a 100644
--- a/etc/schema.mysql
+++ b/etc/schema.mysql
@@ -574,5 +574,5 @@ CREATE TABLE DatabaseSettings (
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 
 CREATE UNIQUE INDEX DatabaseSettings1 ON DatabaseSettings (Name);
-CREATE UNIQUE INDEX DatabaseSettings2 ON DatabaseSettings (Disabled);
+CREATE INDEX DatabaseSettings2 ON DatabaseSettings (Disabled);
 

commit 190dd276798aed70984cd607c869c5dc3560e7e4
Author: michel <michel at bestpractical.com>
Date:   Fri Oct 25 14:54:59 2019 +0200

    Fixes the Callback argument, it is an argument of the widget, not the option.

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 8d628aca7a..1487674e3f 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -1475,6 +1475,16 @@ our %META;
     ValidateUserEmailAddresses => {
         Widget => '/Widgets/Form/Boolean',
     },
+    VERPPrefix => {
+        Widget => '/Widgets/Form/String',
+        WidgetArguments => { Hints  => 'rt-', },
+    },
+    VERPDomain => {
+        Widget => '/Widgets/Form/String',
+        WidgetArguments => {
+            Callback => sub {  return { Hints => RT->Config->Get( 'Organization') } },
+        },
+    },
     WebFallbackToRTLogin => {
         Widget => '/Widgets/Form/Boolean',
     },

commit a4848b86763c4d08c4fc3e6faa2e24f290a8f24d
Author: michel <michel at bestpractical.com>
Date:   Wed Nov 6 15:19:11 2019 +0100

    Updates config edit widgets (bootstrap+defaults)

diff --git a/share/html/Widgets/Form/Boolean b/share/html/Widgets/Form/Boolean
index 3b7091b753..0f8b02a5bd 100644
--- a/share/html/Widgets/Form/Boolean
+++ b/share/html/Widgets/Form/Boolean
@@ -49,18 +49,23 @@
 see docs/extending/using_forms_widgets.pod
 </%DOC>
 <div id="form-box-<% lc $Name %>" class="widget form-row">
-  <div class="col-md-3 label">
-    <% $Description // '' %>
-  </div>
-  <div class="col-md-9 value">
+  <span class="col-md-3 label">
+% if( $LabelLink ) {
+    <a href="<% $LabelLink %>"><% $Description %></a>
+% } else {
+    <% $Description %>
+% }
+  </span>
+  <span class="col-md-9 value">
     <& SELF:InputOnly, %ARGS &>
     <span class="hints"><% $Hints %></span>
-  </div>
+  </span>
 </div>
 <%ARGS>
 $Name         => undef,
 $Description  => undef,
 $Hints        => ''
+$LabelLink    => ''
 </%ARGS>
 
 <%METHOD InputOnly>
diff --git a/share/html/Widgets/Form/Integer b/share/html/Widgets/Form/Integer
index 918fe88b0e..0f26b2200e 100644
--- a/share/html/Widgets/Form/Integer
+++ b/share/html/Widgets/Form/Integer
@@ -49,16 +49,20 @@
 see docs/extending/using_forms_widgets.pod
 </%DOC>
 <div id="form-box-<% lc $Name %>" class="widget form-row">
-  <div class="col-md-3 label">
-    <% $Description // '' %>
-  </div>
-  <div class="col-md-9 value">
+  <span class="col-md-3 label">
+% if( $LabelLink ) {
+    <a href="<% $LabelLink %>"><% $Description %></a>
+% } else {
+    <% $Description %>
+% }
+  </span>
+  <span class="col-md-9 value">
     <& SELF:InputOnly, %ARGS &>
 % if ( $Default ) {
     <span class="comment"><% $DefaultLabel %></span>
 % }
     <span class="hints"><% $Hints %></span>
-  </div>
+  </span>
 </div>
 <%INIT>
 $_ = '' foreach grep !defined, $CurrentValue, $DefaultValue;
@@ -75,6 +79,7 @@ $CurrentValue => '',
 $Default        => 0,
 $DefaultValue   => 0,
 $DefaultLabel   => undef
+$LabelLink      => ''
 </%ARGS>
 
 <%METHOD InputOnly>
diff --git a/share/html/Widgets/Form/MultilineString b/share/html/Widgets/Form/MultilineString
index fa6a74758d..dee3bd260b 100644
--- a/share/html/Widgets/Form/MultilineString
+++ b/share/html/Widgets/Form/MultilineString
@@ -48,17 +48,24 @@
 <%DOC>
 see docs/extending/using_forms_widgets.pod
 </%DOC>
-<div id="form-box-<% lc $Name %>" class="widget <% $Class %>">
-<span class="description label"><% $Description %></span>
-<span class="value"><& SELF:InputOnly, %ARGS &></span>
+<div id="form-box-<% lc $Name %>" class="widget form-row <% $Class %>">
+  <span class="col-md-3 label">
+% if( $LabelLink ) {
+    <a href="<% $LabelLink %>"><% $Description %></a>
+% } else {
+    <% $Description %>
+% }
+  </span>
+<span class="col-md-9 value"><& SELF:InputOnly, %ARGS &>
 % if ( $Default ) {
 <span class="comment"><% $DefaultLabel %></span>
 % }
 <span class="hints"><% $Hints %></span>
+</span>
 </div>
 <%ARGS>
 $Name
-
+$LabelLink    => ''
 $Class        => ''
 $Description  => undef,
 $Hints        => ''
diff --git a/share/html/Widgets/Form/Select b/share/html/Widgets/Form/Select
index 5f8e3e6147..ef144ac8ed 100644
--- a/share/html/Widgets/Form/Select
+++ b/share/html/Widgets/Form/Select
@@ -49,18 +49,23 @@
 see docs/extending/using_forms_widgets.pod
 </%DOC>
 <div id="form-box-<% lc $Name %>" class="widget form-row">
-  <div class="col-md-3 label">
-    <% $Description // '' %>
-  </div>
-  <div class="col-md-9 value">
+  <span class="col-md-3 label">
+% if( $LabelLink ) {
+    <a href="<% $LabelLink %>"><% $Description %></a>
+% } else {
+    <% $Description %>
+% }
+  </span>
+  <span class="col-md-9 value">
     <& SELF:InputOnly, %ARGS &>
     <span class="hints"><% $Hints %></span>
-  </div>
+  </span>
 </div>
 <%ARGS>
 $Name
-$Description      => undef,
+$Description      => '',
 $Hints            => ''
+$LabelLink        => ''
 </%ARGS>
 
 <%METHOD InputOnly>
diff --git a/share/html/Widgets/Form/String b/share/html/Widgets/Form/String
index 24cada9716..cc5339a173 100644
--- a/share/html/Widgets/Form/String
+++ b/share/html/Widgets/Form/String
@@ -49,28 +49,33 @@
 see docs/extending/using_forms_widgets.pod
 </%DOC>
 <div id="form-box-<% lc $Name %>" class="widget form-row">
-  <div class="col-md-3 label">
+  <span class="col-md-3 label">
+% if( $LabelLink ) {
+    <a href="<% $LabelLink %>"><% $Description %></a>
+% } else {
     <% $Description // '' %>
-  </div>
-  <div class="col-md-9 value">
+% }
+  </span>
+  <span class="col-md-9 value">
     <& SELF:InputOnly, %ARGS &>
 % if ( $Default ) {
     <span class="comment"><% $DefaultLabel %></span>
 % }
     <span class="hints"><% $Hints %></span>
-  </div>
+  </span>
 </div>
 <%ARGS>
 $Name
 
-$Description  => undef,
+$Description  => '',
 $Hints        => ''
 
 $CurrentValue => '',
 
 $Default        => 0,
 $DefaultValue   => '',
-$DefaultLabel   => loc( 'Default: [_1]', $DefaultValue ),
+$DefaultLabel   => loc( 'Default: [_1]', $DefaultValue // '' ),
+$LabelLink      => '',
 </%ARGS>
 
 <%METHOD InputOnly>

commit ba37d6b9107d1c37257d1984fec9bf38bc93bd97
Author: michel <michel at bestpractical.com>
Date:   Thu Nov 7 17:11:47 2019 +0100

    Add tabs to the Configuration in DB feature
    
    The tab/section structure is set in Config.pm, subsections
    are defined by parsing RT_Config.pm.
    
    The whole page now uses bootstrap with 4.6 styles.

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 1487674e3f..a6a3688b42 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -56,6 +56,7 @@ use File::Spec ();
 use Symbol::Global::Name;
 use List::MoreUtils 'uniq';
 use Clone ();
+use Pod::Simple::HTML;
 
 # Store log messages generated before RT::Logger is available
 our @PreInitLoggerMessages;
@@ -1886,6 +1887,181 @@ sub PostLoadCheck {
     }
 }
 
+=head2 SectionMap
+
+A data structure used to breakup the option list into tabs/sections/subsections/options
+This is done by parsing RT_Config.pm and extracting the section names and level
+
+=cut
+
+# initial data, manually created to give the tab structure and the order of the sections
+# the result of the parsing of RT_Config.pm will be added to this
+# sections will have content: a list of subsections with a Name and a Content
+our $SectionMap= [
+    { Name    => 'System', # loc
+      Content => [
+          { Name => 'Base configuration' }, # loc
+          { Name => 'Database connection' }, # loc
+          { Name => 'Logging' }, # loc
+          { Name => 'Incoming mail gateway' }, # loc
+          { Name => 'Outgoing mail' }, # loc
+          { Name => 'Application logic' }, # loc
+          { Name => 'Extra security' }, # loc
+          { Name => 'Internationalization' }, # loc
+          { Name => 'Date and time handling' }, # loc
+          { Name => 'Initialdata Formats' }, # loc
+          { Name => 'Development options' }, # loc
+      ],
+    },
+    { Name => 'Web UI', # loc
+      Content => [
+          { Name => 'Web interface' }, # loc
+      ],
+    },
+    { Name => 'Features', # loc
+      Content => [
+          { Name => 'Assets' }, # loc
+          { Name => 'Cryptography' }, # loc
+          { Name => 'External storage' }, # loc
+          { Name => 'SLA' }, # loc
+          { Name => 'Administrative interface' }, # loc
+      ],
+    },
+    { Name => 'User Auth', # loc
+      Content => [
+          { Name => 'Authorization and user configuration' }, # loc
+      ],
+    },
+];
+
+our $SectionMapLoaded=0; # so we only load it once
+
+sub LoadSectionMap {
+    my $self = shift;
+
+    if( $SectionMapLoaded) {
+        return $SectionMap;
+    }
+
+    # create a hash <section> => <tab> / Content so we know in which tab to look for a section
+    my %SectionIndex;
+    foreach my $Tab (@$SectionMap) {
+        my $TabName= $Tab->{Name};
+        foreach my $section ( @{$Tab->{Content}}) {
+            $section->{Content} = [];
+            $SectionIndex{$section->{Name}} = { Tab => $TabName, Content => $section->{Content} };
+        }
+    }
+
+    my $ConfigFile= "$RT::EtcPath/RT_Config.pm";
+    my $PodParser = Pod::Simple::HTML->new();
+
+    my $html;
+    $PodParser->output_string(\$html);
+    $PodParser->parse_file( $ConfigFile);
+
+    my $CurrentTabName;
+    my $CurrentSectionName;
+    my $CurrentSubSectionName;
+    my $CurrentSectionContent;
+
+    while( $html=~ m{<(h[12]|dt)\b[^>]*>(.*?)</\1>}sg) {
+        my( $tag, $content)= ($1, $2);
+        if( $tag eq 'h1') {
+            my ( $id, $title)= $content=~ m{<a class='u'\s*name="([^"]*)"\s*>([^<]*)</a>};
+            next if $title eq 'NAME';
+            $CurrentSectionName = $title;
+            if( $SectionIndex{$CurrentSectionName}->{Tab} ) {
+                $CurrentTabName = $SectionIndex{$CurrentSectionName}->{Tab};
+                # create a sub section with no name, for section level options
+                push @{$SectionIndex{$CurrentSectionName}->{Content}}, { Name => '', Content => [] };
+                $CurrentSectionContent = $SectionIndex{$CurrentSectionName}->{Content};
+                $CurrentSubSectionName='';
+            }
+            else {
+                RT->Logger->debug( "section $CurrentSectionName not found in SectionMap\n");
+            }
+        }
+        elsif( $tag eq 'h2') {
+            my ( $id, $title)= $content=~ m{<a class='u'\s*name="([^"]*)"\s*>([^<]*)</a>};
+            $CurrentSubSectionName= $title;
+            push  @$CurrentSectionContent, { Name => $CurrentSubSectionName, Content => [] };
+        }
+        else {
+            # tag is 'dt'
+            my @options;
+            # a single item (dt) can document several options, in separate <code> elements
+            my( $name)= $content=~ m{name=".([^"]*)"};
+            $name=~ s{,_.}{-}g;
+            while( $content=~ m{<code>(.)([^<]*)</code>}sg) {
+                my( $sigil, $option)= ($1, $2);
+                next unless $sigil=~ m{[\@\%\$]}; # no sigil => this is a value for a select option
+                if( $META{$option}) {
+                    my $LastSubSectionContent = $CurrentSectionContent->[-1]->{Content};
+                    push @$LastSubSectionContent, { Name => $option, Help => $name };
+                }
+                else {
+                    my $TabName= $SectionIndex{$CurrentSectionName}->{Name};
+                    RT->Logger->debug( "missing META info for option [$option]\n");
+                }
+            }
+        }
+    }
+    $SectionMapLoaded = 1;
+    return $SectionMap;
+}
+
+sub name_to_id {
+    my $self = shift;
+    my $name = shift;
+    my $id = lc( $name);
+    $id =~ s{[^a-z0-9]+}{-}g;
+    return $id;
+}
+
+# returns an id in the EditConfig.html page
+# args are
+# type    => ['nav' || 'content' || 'form' ] id for nav links or content respectively
+# level   => ['tab' || 'section' || 'subsection' ]
+# context => { tab => <tab_id>, section => <section_id>, subsection => <subsection_id> }
+sub edit_config_id {
+    my $self = shift;
+    my %args = @_;
+    my $context = $args{context};
+    my @id_components = ($args{type}, $context->{tab});
+    if( $args{level} eq 'section') {
+        push @id_components, $context->{section};
+    }
+    elsif( $args{level} eq 'subsection') {
+        push @id_components, $context->{section}, $context->{subsection};
+    }
+    return join '-', @id_components;
+}
+
+# returns whether a section is active (ie it's displayed when you open its tab) or not
+# if the current tab is active then the section is active if it is the active one
+# otherwise (the current tab is not the active one), the section is active if it is
+# the first one
+# this is abstracted as a method because it needs to be called for each menu item  and
+# for each tab content, so this avoids duplicating code
+# first-section still needs to be managed by the calling code though
+sub section_is_active {
+    my $self= shift;
+    my %args = @_;
+    my $first_section = $args{first_section};
+    my $active_context = $args{active_context};
+    my $current_context = $args{current_context};
+
+    my $active = 0;
+    if( $current_context->{tab} eq $active_context->{tab} ) {
+        $active = 1 if $current_context->{section} eq $active_context->{section};
+     }
+     else {
+        $active = 1 if $first_section;
+    }
+    return $active;
+}
+
 =head2 Configs
 
 Returns list of config files found in local etc, plugins' etc
@@ -2333,6 +2509,7 @@ sub RefreshConfigFromDatabase {
         $in_config_change_txn = 0;
     }
 
+    if( RT->InstallMode ) { return; } # RT can't load the config in the DB if the DB is not there!
     my $needs_update = RT->System->ConfigCacheNeedsUpdate;
     if ($needs_update > $database_config_cache_time) {
         $self->LoadConfigFromDatabase();
diff --git a/share/html/Admin/Tools/Config/Elements/Option b/share/html/Admin/Tools/Config/Elements/Option
new file mode 100644
index 0000000000..a3809a9811
--- /dev/null
+++ b/share/html/Admin/Tools/Config/Elements/Option
@@ -0,0 +1,151 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+
+<%PERL>
+
+use Data::Dumper;
+my $stringify = sub {
+    my $value = shift;
+    return "" if !defined($value);
+
+    local $Data::Dumper::Terse = 1;
+    local $Data::Dumper::Indent = 2;
+    local $Data::Dumper::Sortkeys = 1;
+    my $output = Dumper $value;
+    chomp $output;
+    return $output;
+};
+
+my $doc_version = $RT::VERSION;
+$doc_version =~ s/rc\d+//; # 4.4.2rc1 -> 4.4.2
+$doc_version =~ s/\.\d+-\d+-g\w+$//;  # 4.4.3-1-g123 -> 4.4
+
+my $name = $option->{Name};
+my $meta = RT->Config->Meta( $name );
+return if $meta->{Invisible} || $meta->{Deprecated};
+
+my $has_execute_code = $session{CurrentUser}->HasRight(Right => 'ExecuteCode', Object => RT->System);
+
+my $raw_value = RT->Config->Get( $name );
+my $val = $stringify->($raw_value);
+my $doc_url = "https://docs.bestpractical.com/rt/$doc_version/RT_Config.html#$option->{Help}";
+my $widget = $meta->{'Widget'} || '/Widgets/Form/Code';
+my $is_code = $widget eq '/Widgets/Form/Code';
+my $is_password = ($name =~ /Password/i and $name !~ /MinimumPasswordLength|AllowLoginPasswordAutoComplete/ );
+my $is_immutable = $meta->{Immutable}
+                || $meta->{Obfuscate}
+                || ($is_code && $val =~ s/sub \{ "DUMMY" \}/sub { ... }/g)
+                || ($is_code && !$has_execute_code);
+
+my $current_value = $is_code ? $val : $raw_value;
+my $args   = $meta->{'WidgetArguments'} || {};
+if ($widget eq '/Widgets/Form/Boolean') {
+    %$args = (
+        Default => 0,
+        RadioStyle => 1,
+        %$args,
+    );
+}
+elsif ($widget eq '/Widgets/Form/String' || $widget eq '/Widgets/Form/Integer') {
+    %$args = (
+        Size => 60,
+        %$args,
+    );
+}
+my $row_start = qq{<div class="widget form-row">
+  <span class="col-md-3 label"><a href="$doc_url" target="_blank">$name</a></span>
+  <span class="col-md-9 value">
+};
+my $row_end = qq{</span></div>};
+
+</%PERL>
+
+<!-- start option <% $name %> -->
+% if ( $meta->{EditLink} ) {
+% if ($widget eq '/Widgets/Form/MultilineString' || $widget eq '/Widgets/Form/Code') {
+<% $row_start |n %><textarea disabled class="<% $is_code ? 'code' : '' %>" rows="6" cols="80"><% $current_value %></textarea><br />
+% } else {
+<% $row_start |n %><input type="text" disabled width="80" value="<% $current_value %>" /><br/>
+% }
+<&|/l_unsafe, "<a href=\"$meta->{EditLink}\">", loc($meta->{EditLinkLabel}), "</a>" &>Visit [_1][_2][_3] to manage this setting</&>
+% } elsif ( $name =~ /Plugins/) {
+<% $row_start |n %><ul class="plugins">
+% for my $plugin (RT->Config->Get($name)) {
+<li><a href="https://metacpan.org/search?q=<% $plugin |u %>" target="_blank"><% $plugin %></a></li>
+% }
+</ul><% $row_end |n%>
+<br /><em><% loc('Must modify in config file' ) %></em>
+% } elsif ( $is_password ) {
+<em><% loc('Must modify in config file' ) %></em><br />
+% } elsif ( $is_immutable ) {
+% if ($widget eq '/Widgets/Form/MultilineString' || $widget eq '/Widgets/Form/Code') {
+<% $row_start |n %><textarea disabled class="<% $is_code ? 'code' : '' %>" rows="6" cols="80"><% $current_value %></textarea>
+% } else {
+<% $row_start |n %><input type="text" disabled width="80" value="<% $current_value %>" />
+% }
+<br /><em><% loc('Must modify in config file' ) %></em>
+<% $row_end |n %>
+% } else { 
+  <& $widget,
+    Default      => 1,
+    DefaultValue => '',
+    DefaultLabel => '(no value)',
+    Name         => $name,
+    LabelLink    => $doc_url,
+    CurrentValue => $current_value,
+    Description  => $name,
+    Hints        => $meta->{WidgetArguments}->{Hints} || '',
+    %{ $m->comp('/Widgets/FinalizeWidgetArguments', WidgetArguments =>
+            $meta->{'WidgetArguments'} ) },
+  &>
+<textarea class="hidden" name="<% $name %>-Current"><% $current_value %></textarea>
+% }
+<!-- end option <% $name %> -->
+<%ARGS>
+$option
+</%ARGS>
diff --git a/share/html/Widgets/FinalizeWidgetArguments b/share/html/Admin/Tools/Config/Elements/Section
similarity index 79%
copy from share/html/Widgets/FinalizeWidgetArguments
copy to share/html/Admin/Tools/Config/Elements/Section
index f742fb4548..98ebbc0555 100644
--- a/share/html/Widgets/FinalizeWidgetArguments
+++ b/share/html/Admin/Tools/Config/Elements/Section
@@ -45,21 +45,16 @@
 %# those contributions and any derivatives thereof.
 %#
 %# END BPS TAGGED BLOCK }}}
-<%init>
-    my %args = %$WidgetArguments;
 
-    %args = (%args, %{ $args{Callback}->() }) if $args{Callback};
-    $args{'Description'} = loc( $args{'Description'} ) if $args{'Description'};
-    $args{'Hints'} = loc( $args{'Hints'} ) if $args{'Hints'};
-    if ( $args{'ValuesLabel'} ) {
-        my %labels;
-        $labels{$_} = loc( $args{'ValuesLabel'}->{$_} )
-            for keys %{$args{'ValuesLabel'}};
-        $args{'ValuesLabel'} = \%labels;
-    }
-    return \%args;
-</%init>
+% my $section_id= RT->Config->name_to_id( $section->{Name} );
 
-<%args>
-$WidgetArguments => {}
-</%args>
+% foreach my $subsection ( @{$section->{Content}} ) {
+%     $current_context->{subsection}= RT->Config->name_to_id( $subsection->{Name});
+  <& /Admin/Tools/Config/Elements/SubSection, subsection => $subsection, active_context => $active_context, current_context => $current_context &>
+  <!-- end subsection <% $subsection->{Name} %> -->
+% }
+<%ARGS>
+$section
+$active_context
+$current_context
+</%ARGS>
diff --git a/share/html/Widgets/FinalizeWidgetArguments b/share/html/Admin/Tools/Config/Elements/SubSection
similarity index 65%
copy from share/html/Widgets/FinalizeWidgetArguments
copy to share/html/Admin/Tools/Config/Elements/SubSection
index f742fb4548..2ebe703385 100644
--- a/share/html/Widgets/FinalizeWidgetArguments
+++ b/share/html/Admin/Tools/Config/Elements/SubSection
@@ -45,21 +45,33 @@
 %# those contributions and any derivatives thereof.
 %#
 %# END BPS TAGGED BLOCK }}}
-<%init>
-    my %args = %$WidgetArguments;
 
-    %args = (%args, %{ $args{Callback}->() }) if $args{Callback};
-    $args{'Description'} = loc( $args{'Description'} ) if $args{'Description'};
-    $args{'Hints'} = loc( $args{'Hints'} ) if $args{'Hints'};
-    if ( $args{'ValuesLabel'} ) {
-        my %labels;
-        $labels{$_} = loc( $args{'ValuesLabel'}->{$_} )
-            for keys %{$args{'ValuesLabel'}};
-        $args{'ValuesLabel'} = \%labels;
-    }
-    return \%args;
-</%init>
-
-<%args>
-$WidgetArguments => {}
-</%args>
+% if( @{$subsection->{Content}}) {
+% my $id = RT->Config->edit_config_id( type => 'form', level => 'subsection', context => $current_context);
+<form id="<% $id %>" name="EditConfig" method="post" action="EditConfig.html#<% $id %>">
+    <input type="hidden" name="Update" value="1" />
+% my $complete_title = 
+%     join ' - ', map { s{-}{ }g; loc( ucfirst $_ ) } 
+%     grep { $_ }
+%     map { $current_context->{$_} }
+%     (qw( tab section subsection ) );
+<&|/Widgets/TitleBox, title => $complete_title &>
+% foreach my $option ( @{$subsection->{Content}} ) {
+  <& /Admin/Tools/Config/Elements/Option, option => $option &>
+% }
+<input type="hidden" name="tab" value="<% $current_context->{tab} %>" />
+<input type="hidden" name="section" value="<% $current_context->{section} %>" />
+<input type="hidden" name="subsection" value="<% $current_context->{subsection} %>" />
+<div class="form-row">
+  <span class="col-md-12">
+    <& /Elements/Submit, Label => loc('Save Changes') &>
+  </span>
+</div>
+</&>
+</form>
+% }
+<%ARGS>
+$subsection
+$active_context
+$current_context
+</%ARGS>
diff --git a/share/html/Admin/Tools/Config/Elements/Tab b/share/html/Admin/Tools/Config/Elements/Tab
new file mode 100644
index 0000000000..b5e27e643d
--- /dev/null
+++ b/share/html/Admin/Tools/Config/Elements/Tab
@@ -0,0 +1,101 @@
+%# BEGIN BPS TAGGED BLOCK {{{
+%#
+%# COPYRIGHT:
+%#
+%# This software is Copyright (c) 1996-2019 Best Practical Solutions, LLC
+%#                                          <sales at bestpractical.com>
+%#
+%# (Except where explicitly superseded by other copyright notices)
+%#
+%#
+%# LICENSE:
+%#
+%# This work is made available to you under the terms of Version 2 of
+%# the GNU General Public License. A copy of that license should have
+%# been provided with this software, but in any event can be snarfed
+%# from www.gnu.org.
+%#
+%# This work is distributed in the hope that it will be useful, but
+%# WITHOUT ANY WARRANTY; without even the implied warranty of
+%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+%# General Public License for more details.
+%#
+%# You should have received a copy of the GNU General Public License
+%# along with this program; if not, write to the Free Software
+%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+%# 02110-1301 or visit their web page on the internet at
+%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
+%#
+%#
+%# CONTRIBUTION SUBMISSION POLICY:
+%#
+%# (The following paragraph is not intended to limit the rights granted
+%# to you to modify and distribute this software under the terms of
+%# the GNU General Public License and is only of importance to you if
+%# you choose to contribute your changes and enhancements to the
+%# community by submitting them to Best Practical Solutions, LLC.)
+%#
+%# By intentionally submitting any modifications, corrections or
+%# derivatives to this work, or any other work intended for use with
+%# Request Tracker, to Best Practical Solutions, LLC, you confirm that
+%# you are the copyright holder for those contributions and you grant
+%# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
+%# royalty-free, perpetual, license to use, copy, create derivative
+%# works based on those contributions, and sublicense and distribute
+%# those contributions and any derivatives thereof.
+%#
+%# END BPS TAGGED BLOCK }}}
+
+% my $nav_type = 'pill'; # 'tab' or 'pill'
+% my $tab_id= RT->Config->name_to_id( $tab->{Name} );
+<div class="row">
+  <div class="col-3">
+% my @section_names = map { $_->{Name} } @{$tab->{Content}};
+    <ul class="nav nav-<% $nav_type %>s flex-column navbar-fixed-top" id="config-sections-<% $tab_id %>" aria-orientation="vertical">
+% my $first_section = 1;
+% foreach my $section_name (@section_names) {
+%     my $section_id= RT->Config->name_to_id( $section_name );
+%     $current_context->{section}= $section_id;
+%     my $active = RT->Config->section_is_active( 
+%         first_section => $first_section,
+%         active_context => $active_context,
+%         current_context => $current_context,
+%     );
+%     $first_section = 0;
+%     my( $active_class, $aria_selected) = $active ? ('active', 'true') : ('', 'false');
+%     my $nav_id = RT->Config->edit_config_id( type => 'nav', level => 'section', context => $current_context); 
+%     my $content_id = RT->Config->edit_config_id( type => 'content', level => 'section', context => $current_context); 
+      <li class="nav-item">
+        <a class="nav-link <% $active_class %>" id="<% $nav_id %>" data-toggle="<% $nav_type %>" href="#<% $content_id %>" role="<% $nav_type %>" aria-controls="<% $nav_id %>" aria-selected="<% $aria_selected %>"><% $section_name %></a>
+      </li>
+% }
+    </ul>
+  </div>
+  <div class="col-9">
+    <div class="tab-content" id="tab-content-<% $tab_id %>" >
+
+% $first_section = 1;
+% foreach my $section ( @{$tab->{Content}} ) {
+%     my $section_id= RT->Config->name_to_id( $section->{Name} );
+%     $current_context->{section}= $section_id;
+%     my $active = RT->Config->section_is_active( 
+%         first_section => $first_section,
+%         active_context => $active_context,
+%         current_context => $current_context,
+%     );
+%     my $active_class = $active ? 'active show' : '';
+%     $first_section = 0;
+%     my $nav_id = RT->Config->edit_config_id( type => 'nav', level => 'section', context => $current_context); 
+%     my $content_id = RT->Config->edit_config_id( type => 'content', level => 'section', context => $current_context); 
+      <div class="tab-pane fade <% $active_class %>" role="tabpanel" id="<% $content_id %>" aria-labelledby="<% $nav_id %>">
+        <& /Admin/Tools/Config/Elements/Section, section => $section, current_context => $current_context, active_context => $active_context &>
+      </div><!-- end section <% $content_id %> -->
+% }
+    </div><!-- end of tab tab-content-<% $tab_id %> -->
+  </div>
+</div>
+<%ARGS>
+$tab
+$active_context
+$current_context
+</%ARGS>
diff --git a/share/html/Admin/Tools/EditConfig.html b/share/html/Admin/Tools/EditConfig.html
index 62adc78734..c52d9c0816 100644
--- a/share/html/Admin/Tools/EditConfig.html
+++ b/share/html/Admin/Tools/EditConfig.html
@@ -53,6 +53,13 @@ unless ($session{'CurrentUser'}->HasRight( Object=> $RT::System, Right => 'Super
 
 my $has_execute_code = $session{CurrentUser}->HasRight(Right => 'ExecuteCode', Object => RT->System);
 
+my $options = RT->Config->LoadSectionMap();
+my $active_context = { 
+    tab        => RT->Config->name_to_id( $ARGS{tab}        || $options->[0]->{Name}) ,
+    section    => RT->Config->name_to_id( $ARGS{section}    || $options->[0]->{Content}->[0]->{Name}) ,
+    subsection => RT->Config->name_to_id( $ARGS{subsection} || $options->[0]->{Content}->[0]->{Content}->[0]->{Name}) ,
+};
+
 my @results;
 
 my $doc_version = $RT::VERSION;
@@ -80,6 +87,7 @@ if (delete $ARGS{Update}) {
     eval {
         for my $key (keys %ARGS) {
             next if $key =~ /-Current$/;
+            next if $key eq 'tab' || $key eq 'section' || $key eq 'subsection';
 
             my $meta = RT->Config->Meta( $key );
             my $widget = $meta->{Widget} || '/Widgets/Form/Code';
@@ -170,106 +178,37 @@ if (delete $ARGS{Update}) {
     RT->Config->EndDatabaseConfigChanges;
 }
 
+my $nav_type='tab'; # 'tab' or 'pill'
+
 </%INIT>
 <& /Admin/Elements/Header, Title => $title &>
 <& /Elements/Tabs &>
 <& /Elements/ListActions, actions => \@results &>
 
-<form id="EditConfig" method="post" action="EditConfig.html">
-<input type="hidden" name="Update" value=1></input>
-
-<&|/Widgets/TitleBox, title => loc("RT Configuration") &>
-<table border="0" cellspacing="0" cellpadding="5" width="100%" class="collection">
-<tr class="collection-as-table">
-<th class="collection-as-table"><&|/l&>Option</&></th>
-<th class="collection-as-table"><&|/l&>Value</&></th>
-</tr>
-<%PERL>
-my $index_conf;
-foreach my $key ( RT->Config->Options( Overridable => undef, Sorted => 0 ) ) {
-    my $meta = RT->Config->Meta( $key );
-
-    next if $meta->{Invisible} || $meta->{Deprecated};
-
-    my $raw_value = RT->Config->Get( $key );
-    my $val = $stringify->($raw_value);
-
-    $index_conf++;
-
-    my $doc_url = "https://docs.bestpractical.com/rt/$doc_version/RT_Config.html#$key";
-
-    my $widget = $meta->{'Widget'} || '/Widgets/Form/Code';
-    my $is_code = $widget eq '/Widgets/Form/Code';
-    my $is_password = ($key =~ /Password/i and $key !~ /MinimumPasswordLength|AllowLoginPasswordAutoComplete/ );
-    my $is_immutable = $meta->{Immutable}
-                    || $meta->{Obfuscate}
-                    || ($is_code && $val =~ s/sub { "DUMMY" }/sub { ... }/g)
-                    || ($is_code && !$has_execute_code);
-
-    my $current_value = $is_code ? $val : $raw_value;
-    my $args   = $meta->{'WidgetArguments'} || {};
-
-    if ($widget eq '/Widgets/Form/Boolean') {
-        %$args = (
-            Default => 0,
-            RadioStyle => 1,
-            %$args,
-        );
-    }
-    elsif ($widget eq '/Widgets/Form/String' || $widget eq '/Widgets/Form/Integer') {
-        %$args = (
-            Size => 60,
-            %$args,
-        );
-    }
-
-</%PERL>
-<tr class="<% $key %> <% $index_conf%2 ? 'oddline' : 'evenline'%>">
-<td class="collection-as-table"><a href="<% $doc_url %>" target="_blank"><% $key %></a></td>
-<td class="collection-as-table">
-
-% if ( $meta->{EditLink} ) {
-% if ($widget eq '/Widgets/Form/MultilineString' || $widget eq '/Widgets/Form/Code') {
-<textarea disabled class="<% $is_code ? 'code' : '' %>" rows="6" cols="80"><% $current_value %></textarea><br>
-% } else {
-<input type="text" disabled width="80" value="<% $current_value %>"></input><br>
-% }
-<&|/l_unsafe, "<a href=\"$meta->{EditLink}\">", loc($meta->{EditLinkLabel}), "</a>" &>Visit [_1][_2][_3] to manage this setting</&>
-% } elsif ( $key =~ /Plugins/) {
-<ul class="plugins">
-% for my $plugin (RT->Config->Get($key)) {
-<li><a href="https://metacpan.org/search?q=<% $plugin |u %>" target="_blank"><% $plugin %></a></li>
+<div class="titlebox-content">
+% my @tab_names = map { $_->{Name} } @$options;
+<ul class="nav nav-<% $nav_type %>s" id="config-tabs">
+% my $current_context = {};
+% foreach my $tab_name (@tab_names) {
+%     my $tab_id= RT->Config->name_to_id( $tab_name );
+%     $current_context->{tab}= $tab_id;
+%     my( $active, $aria_selected) = $tab_id eq $active_context->{tab} ? ('active', 'true') : ('', 'false');
+%     my $nav_id = RT->Config->edit_config_id( type => 'nav', level => 'tab', context => $current_context); 
+%     my $content_id = RT->Config->edit_config_id( type => 'content', level => 'tab', context => $current_context); 
+  <li class="nav-item">
+    <a class="nav-link <% $active %>" id="<% $nav_id %>" data-toggle="<% $nav_type %>" href="#<% $content_id %>" role="<% $nav_type %>" aria-controls="<% $content_id %>" aria-selected="<% $aria_selected %>"><% $tab_name %></a>
+  </li>
 % }
 </ul>
-<br><em><% loc('Must modify in config file' ) %></em>
-% } elsif ( $is_password ) {
-<em><% loc('Must modify in config file' ) %></em>
-% } elsif ( $is_immutable ) {
-% if ($widget eq '/Widgets/Form/MultilineString' || $widget eq '/Widgets/Form/Code') {
-<textarea disabled class="<% $is_code ? 'code' : '' %>" rows="6" cols="80"><% $current_value %></textarea>
-% } else {
-<input type="text" disabled width="80" value="<% $current_value %>"></input>
-% }
-<br><em><% loc('Must modify in config file' ) %></em>
-% } else {
-  <& $widget,
-    Default      => 1,
-    DefaultValue => '',
-    DefaultLabel => '(no value)',
-
-    %{ $m->comp('/Widgets/FinalizeWidgetArguments', WidgetArguments => $args ) },
-    Name         => $key,
-    CurrentValue => $current_value,
-    Description  => '',
-    Hints        => '',
-  &>
-<textarea class="hidden" name="<% $key %>-Current"><% $current_value %></textarea>
+<div class="tab-content" id="content-all" >
+% foreach my $tab ( @$options) {
+%     my $tab_id= RT->Config->name_to_id( $tab->{Name} );
+%     $current_context->{tab}= $tab_id;
+%     my $active = $tab_id eq $active_context->{tab} ? ' show active' : '';
+%     my $nav_id = RT->Config->edit_config_id( type => 'nav', level => 'tab', context => $current_context); 
+%     my $content_id = RT->Config->edit_config_id( type => 'content', level => 'tab', context => $current_context); 
+<div class="tab-pane fade<% $active %>" role="tabpanel" id="<% $content_id %>" aria-labelledby="<% $nav_id %>">
+  <& /Admin/Tools/Config/Elements/Tab, tab => $tab, active_context => $active_context, current_context => $current_context &>
+</div><!-- <% $content_id %> -->
 % }
-</td>
-</tr>
-% }
-</table>
-</&>
-<& /Elements/Submit, Label => loc('Save Changes') &>
-</form>
-
+</div><!-- content-all -->
diff --git a/share/html/Widgets/FinalizeWidgetArguments b/share/html/Widgets/FinalizeWidgetArguments
index f742fb4548..a96b339432 100644
--- a/share/html/Widgets/FinalizeWidgetArguments
+++ b/share/html/Widgets/FinalizeWidgetArguments
@@ -46,7 +46,7 @@
 %#
 %# END BPS TAGGED BLOCK }}}
 <%init>
-    my %args = %$WidgetArguments;
+    my %args = $WidgetArguments ? %$WidgetArguments : ();
 
     %args = (%args, %{ $args{Callback}->() }) if $args{Callback};
     $args{'Description'} = loc( $args{'Description'} ) if $args{'Description'};
diff --git a/share/html/Widgets/Form/Boolean b/share/html/Widgets/Form/Boolean
index 0f8b02a5bd..9129032cc4 100644
--- a/share/html/Widgets/Form/Boolean
+++ b/share/html/Widgets/Form/Boolean
@@ -56,7 +56,7 @@ see docs/extending/using_forms_widgets.pod
     <% $Description %>
 % }
   </span>
-  <span class="col-md-9 value">
+  <span class="col-md-9">
     <& SELF:InputOnly, %ARGS &>
     <span class="hints"><% $Hints %></span>
   </span>
diff --git a/share/html/Widgets/Form/MultilineString b/share/html/Widgets/Form/MultilineString
index dee3bd260b..642d512962 100644
--- a/share/html/Widgets/Form/MultilineString
+++ b/share/html/Widgets/Form/MultilineString
@@ -78,7 +78,7 @@ $DefaultLabel   => loc( 'Default: [_1]', $DefaultValue ),
 </%ARGS>
 
 <%METHOD InputOnly>
-<textarea name="<% $Name %>" cols="<% $Cols %>" rows="<% $Rows %>"><% $CurrentValue %></textarea>
+<textarea name="<% $Name %>" class="form-control" cols="<% $Cols %>" rows="<% $Rows %>"><% $CurrentValue %></textarea>
 <%ARGS>
 $Name
 $Cols => 80

commit 1d87ddbb831682eb8c390a121dcb71bd64994648
Author: michel <michel at bestpractical.com>
Date:   Thu Nov 7 17:17:14 2019 +0100

    Add missing config options to %META
    
    Previously config options did not have to be added to %META, config
    in UI now requires it to generate the input form.

diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index a6a3688b42..e47b87452e 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -609,6 +609,24 @@ our %META;
             }
         },
     },
+    CanonicalizeEmailAddressMatch => {
+        Section         => 'Mail',                                     #loc
+        Type            => 'SCALAR',
+        Overridable     => 1,
+        Widget          => '/Widgets/Form/String',
+    },
+    CanonicalizeEmailAddressReplace => {
+        Section         => 'Mail',                                     #loc
+        Type            => 'SCALAR',
+        Overridable     => 1,
+        Widget          => '/Widgets/Form/String',
+    },
+    EmailSubjectTagRegex => {
+        Section         => 'Mail',                                     #loc
+        Type            => 'SCALAR',
+        Overridable     => 1,
+        Widget          => '/Widgets/Form/String',
+    },
     # User overridable mail options
     EmailFrequency => {
         Section         => 'Mail',                                     #loc
@@ -1323,6 +1341,12 @@ our %META;
             }
         },
     },
+    UserAutocreateDefaultsOnLogin => {
+        Type => 'HASH',
+    },
+    AutoCreateNonExternalUsers => {
+        Widget => '/Widgets/Form/Boolean',
+    },
     ServiceAgreements => {
         Type => 'HASH',
     },
@@ -1629,7 +1653,18 @@ our %META;
     WebImagesURL => {
         Widget => '/Widgets/Form/String',
     },
-
+    AssetQueues => {
+        Type => 'ARRAY',
+        Hints  => '',
+    },
+    AssetBasicCustomFieldsOnCreate => {
+        Type => 'ARRAY',
+        Hints  => '[ "foo", "bar"]',
+    },
+    DefaultCatalog => {
+        Widget => '/Widgets/Form/String',
+        Hints  => 'General assets',
+    },
     AssetSearchFormat => {
         Widget => '/Widgets/Form/MultilineString',
     },

-----------------------------------------------------------------------


More information about the rt-commit mailing list