[Rt-commit] rt branch, 4.6/config-in-database, created. rt-4.4.1-255-g94b465b

Shawn Moore shawn at bestpractical.com
Fri Sep 22 14:12:51 EDT 2017


The branch, 4.6/config-in-database has been created
        at  94b465b1082b8fc4bfc2c958018e29991db919aa (commit)

- Log -----------------------------------------------------------------
commit 25b33fd335e241f75ac6cb9dcb233284a5e6a3b9
Merge: 3e15209 c566c73
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Aug 15 19:18:12 2017 +0000

    Merge branch '4.4/widget-improvements' into 4.6/config-in-database


commit 6b2146c091da3b44b13e6a9a67380f4b58018b73
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 6558550..7c2a975 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 afa2c94..a54544a 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 c758284..fb116da 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 3288a57..007b1c4 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 4baf28d..0cd1c69 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 0000000..6a73f0c
--- /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
new file mode 100644
index 0000000..ad05bc7
--- /dev/null
+++ b/etc/upgrade/4.5.0/schema.Oracle
@@ -0,0 +1,16 @@
+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
new file mode 100644
index 0000000..73f4d96
--- /dev/null
+++ b/etc/upgrade/4.5.0/schema.Pg
@@ -0,0 +1,17 @@
+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 0000000..a25a8c9
--- /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
new file mode 100644
index 0000000..177d01e
--- /dev/null
+++ b/etc/upgrade/4.5.0/schema.mysql
@@ -0,0 +1,16 @@
+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 f0352c3266e9ae96f97e86fdab8ab4cd3f9d1f11
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 0000000..fc38e87
--- /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 0000000..3236bad
--- /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 8f42e1cfa868649a418caab535a223d4a0f9a88d
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 95911ca..d80f3f1 100644
--- a/lib/RT.pm
+++ b/lib/RT.pm
@@ -195,6 +195,7 @@ sub Init {
     ConnectToDatabase();
     InitSystemObjects();
     InitClasses(%args);
+    RT->Config->LoadConfigFromDatabase();
     InitLogging();
     InitPlugins();
     _BuildTableAttributes();
@@ -485,6 +486,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 3407796..f662824 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;
 
 =head1 NAME
 
@@ -1786,6 +1787,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 734939a..bba3e3d 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -267,6 +267,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 f5c2d39..08d79dd 100644
--- a/lib/RT/System.pm
+++ b/lib/RT/System.pm
@@ -235,6 +235,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 4dfa52c11c0492cdd2b744bf2687f673ae22379e
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 0000000..1c346d6
--- /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 0000000..fa6a747
--- /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 538dd5e824cc6b0f087ba3f3b4a377b4ac83bea5
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 d68ad6c..5043bf8 100644
--- a/lib/RT/Interface/Web/MenuBuilder.pm
+++ b/lib/RT/Interface/Web/MenuBuilder.pm
@@ -925,6 +925,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 0000000..c3185ed
--- /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>
+
diff --git a/share/static/css/base/forms.css b/share/static/css/base/forms.css
index 2584ee0..3fe379e 100644
--- a/share/static/css/base/forms.css
+++ b/share/static/css/base/forms.css
@@ -280,3 +280,23 @@ ul.selectable a {
     font-weight: bold;
     text-decoration: underline;
 }
+
+/* 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 a93c15b2d6f11729c3bfa90d7079c89b6ba76b97
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 f662824..e87a6ea 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -615,6 +615,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.")
@@ -622,9 +623,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 => {
@@ -718,8 +748,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;
 
@@ -845,6 +891,7 @@ our %META;
     EmailDashboardLanguageOrder  => { Type => 'ARRAY' },
     CustomFieldValuesCanonicalizers => { Type => 'ARRAY' },
     WebPath => {
+        Immutable     => 1,
         PostLoadCheck => sub {
             my $self  = shift;
             my $value = shift;
@@ -871,6 +918,7 @@ our %META;
         },
     },
     WebDomain => {
+        Immutable => 1,
         PostLoadCheck => sub {
             my $self  = shift;
             my $value = shift;
@@ -897,6 +945,7 @@ our %META;
         },
     },
     WebPort => {
+        Immutable     => 1,
         PostLoadCheck => sub {
             my $self  = shift;
             my $value = shift;
@@ -912,6 +961,7 @@ our %META;
         },
     },
     WebBaseURL => {
+        Immutable     => 1,
         PostLoadCheck => sub {
             my $self  = shift;
             my $value = shift;
@@ -935,6 +985,7 @@ our %META;
         },
     },
     WebURL => {
+        Immutable     => 1,
         PostLoadCheck => sub {
             my $self  = shift;
             my $value = shift;
@@ -1049,7 +1100,12 @@ our %META;
         },
     },
 
+    ExternalAuth => {
+        Immutable => 1,
+    },
+
     ExternalSettings => {
+        Immutable     => 1,
         Obfuscate => sub {
             # Ensure passwords are obfuscated on the System Configuration page
             my ($config, $sources, $user) = @_;
@@ -1113,6 +1169,7 @@ our %META;
     },
 
     ExternalAuthPriority => {
+        Immutable     => 1,
         PostLoadCheck => sub {
             my $self = shift;
             my @values = @{ shift || [] };
@@ -1141,6 +1198,7 @@ our %META;
     },
 
     ExternalInfoPriority => {
+        Immutable     => 1,
         PostLoadCheck => sub {
             my $self = shift;
             my @values = @{ shift || [] };

commit f25747d25b8d8004c7f8bb480ffac43f93a46ebb
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 e87a6ea..806f3e2 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -616,6 +616,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.")
@@ -625,6 +626,7 @@ our %META;
 
     rtname => {
         Immutable => 1,
+        Widget    => '/Widgets/Form/String',
     },
 
     # Internal config options
@@ -634,27 +636,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 => {
@@ -713,6 +723,7 @@ our %META;
     },
     DisableGraphViz => {
         Type            => 'SCALAR',
+        Widget          => '/Widgets/Form/Boolean',
         PostLoadCheck   => sub {
             my $self  = shift;
             my $value = shift;
@@ -724,6 +735,7 @@ our %META;
     },
     DisableGD => {
         Type            => 'SCALAR',
+        Widget          => '/Widgets/Form/Boolean',
         PostLoadCheck   => sub {
             my $self  = shift;
             my $value = shift;
@@ -735,6 +747,7 @@ our %META;
     },
     MailCommand => {
         Type    => 'SCALAR',
+        Widget  => '/Widgets/Form/String',
         PostLoadCheck => sub {
             my $self = shift;
             my $value = $self->Get('MailCommand');
@@ -746,6 +759,7 @@ our %META;
     },
     HTMLFormatter => {
         Type => 'SCALAR',
+        Widget => '/Widgets/Form/String',
         PostLoadCheck => sub { RT::Interface::Email->_HTMLFormatter },
     },
     Plugins => {
@@ -753,15 +767,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',
@@ -892,6 +910,7 @@ our %META;
     CustomFieldValuesCanonicalizers => { Type => 'ARRAY' },
     WebPath => {
         Immutable     => 1,
+        Widget        => '/Widgets/Form/String',
         PostLoadCheck => sub {
             my $self  = shift;
             my $value = shift;
@@ -918,7 +937,8 @@ our %META;
         },
     },
     WebDomain => {
-        Immutable => 1,
+        Immutable     => 1,
+        Widget        => '/Widgets/Form/String',
         PostLoadCheck => sub {
             my $self  = shift;
             my $value = shift;
@@ -946,6 +966,7 @@ our %META;
     },
     WebPort => {
         Immutable     => 1,
+        Widget        => '/Widgets/Form/Integer',
         PostLoadCheck => sub {
             my $self  = shift;
             my $value = shift;
@@ -962,6 +983,7 @@ our %META;
     },
     WebBaseURL => {
         Immutable     => 1,
+        Widget        => '/Widgets/Form/String',
         PostLoadCheck => sub {
             my $self  = shift;
             my $value = shift;
@@ -986,6 +1008,7 @@ our %META;
     },
     WebURL => {
         Immutable     => 1,
+        Widget => '/Widgets/Form/String',
         PostLoadCheck => sub {
             my $self  = shift;
             my $value = shift;
@@ -1102,6 +1125,7 @@ our %META;
 
     ExternalAuth => {
         Immutable => 1,
+        Widget    => '/Widgets/Form/Boolean',
     },
 
     ExternalSettings => {
@@ -1232,6 +1256,357 @@ our %META;
             $self->Set( 'ExternalInfoPriority', \@values );
         },
     },
+
+    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 c4a4560825d8a1ebb785aff5f8d004ae44315d89
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 c3185ed..c606d5e 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 1c08a82bd4d49f56bacc7bbf22d1702f3cbe30e9
Author: Shawn M Moore <shawn at bestpractical.com>
Date:   Tue Aug 15 19:54:47 2017 +0000

    Mark ConfigInDatabase as cored

diff --git a/lib/RT.pm b/lib/RT.pm
index d80f3f1..7aee53a 100644
--- a/lib/RT.pm
+++ b/lib/RT.pm
@@ -742,6 +742,8 @@ our %CORED_PLUGINS = (
     'RT::Extension::SpawnLinkedTicketInQueue' => '4.4',
     'RT::Extension::ParentTimeWorked' => '4.4',
     'RT::Extension::FutureMailgate' => '4.4',
+
+    'RT::Extension::ConfigInDatabase' => '4.6',
 );
 
 sub InitPlugins {

commit 8ce972c3ba4a8ad6507a3cc4e734d79eb78c767e
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 806f3e2..6b34afb 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -2062,7 +2062,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'} } );
@@ -2312,6 +2312,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 26ffcc8..1cb918d 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 94b465b1082b8fc4bfc2c958018e29991db919aa
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 c606d5e..62adc78 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">

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


More information about the rt-commit mailing list