X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=rt%2Flib%2FRT%2FConfig.pm;h=ea8ad61abf9f38649b93388008a6268b246f39bc;hp=cc47df35adf2edcb3b0f143b213915003121ac18;hb=187086c479a09629b7d180eec513fb7657f4e291;hpb=43a06151e47d2c59b833cbd8c26d97865ee850b6 diff --git a/rt/lib/RT/Config.pm b/rt/lib/RT/Config.pm index cc47df35a..ea8ad61ab 100644 --- a/rt/lib/RT/Config.pm +++ b/rt/lib/RT/Config.pm @@ -2,7 +2,7 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2018 Best Practical Solutions, LLC # # # (Except where explicitly superseded by other copyright notices) @@ -51,8 +51,10 @@ package RT::Config; use strict; use warnings; - +use 5.010; use File::Spec (); +use Symbol::Global::Name; +use List::MoreUtils 'uniq'; =head1 NAME @@ -107,7 +109,7 @@ Keyed by config name, there are several properties that can be set for each config optin: Section - What header this option should be grouped - under on the user Settings page + under on the user Preferences page Overridable - Can users change this option SortOrder - Within a Section, how should the options be sorted for display to the user @@ -122,6 +124,11 @@ can be set for each config optin: Callback - subref that receives no arguments. It returns a hashref of items that are added to the rest of the WidgetArguments + PostSet - subref passed the RT::Config object and the current and + previous setting of the config option. This is called well + before much of RT's subsystems are initialized, so what you + can do here is pretty limited. It's mostly useful for + effecting the value of other config options early. PostLoadCheck - subref passed the RT::Config object and the current setting of the config option. Can make further checks (such as seeing if a library is installed) and then change @@ -133,8 +140,17 @@ can be set for each config optin: =cut -our %META = ( +our %META; +%META = ( # General user overridable options + RestrictReferrerLogin => { + PostLoadCheck => sub { + my $self = shift; + if (defined($self->Get('RestrictReferrerLogin'))) { + RT::Logger->error("The config option 'RestrictReferrerLogin' is incorrect, and should be 'RestrictLoginReferrer' instead."); + } + }, + }, DefaultQueue => { Section => 'General', Overridable => 1, @@ -171,8 +187,9 @@ our %META = ( Widget => '/Widgets/Form/Select', WidgetArguments => { Description => 'Username format', # loc - Values => [qw(concise verbose)], + Values => [qw(role concise verbose)], ValuesLabel => { + role => 'Privileged: usernames; Unprivileged: names and email addresses', # loc concise => 'Short usernames', # loc verbose => 'Name and email address', # loc }, @@ -195,26 +212,54 @@ our %META = ( Widget => '/Widgets/Form/Select', WidgetArguments => { Description => 'Theme', #loc - # XXX: we need support for 'get values callback' - Values => [qw(web2 freeside2.1 freeside3 aileron ballard)], + Callback => sub { + state @stylesheets; + unless (@stylesheets) { + for my $static_path ( RT::Interface::Web->StaticRoots ) { + my $css_path = + File::Spec->catdir( $static_path, 'css' ); + next unless -d $css_path; + if ( opendir my $dh, $css_path ) { + push @stylesheets, grep { + $_ ne 'base' && -e File::Spec->catfile( $css_path, $_, 'main.css' ) + } readdir $dh; + } + else { + RT->Logger->error("Can't read $css_path: $!"); + } + } + @stylesheets = sort { lc $a cmp lc $b } uniq @stylesheets; + } + return { Values => \@stylesheets }; + }, }, PostLoadCheck => sub { my $self = shift; my $value = $self->Get('WebDefaultStylesheet'); - my @comp_roots = RT::Interface::Web->ComponentRoots; - for my $comp_root (@comp_roots) { - return if -d $comp_root.'/NoAuth/css/'.$value; + my @roots = RT::Interface::Web->StaticRoots; + for my $root (@roots) { + return if -d "$root/css/$value"; } $RT::Logger->warning( "The default stylesheet ($value) does not exist in this instance of RT. " - . "Defaulting to aileron." + . "Defaulting to freeside4." ); - $self->Set('WebDefaultStylesheet', 'aileron'); + $self->Set('WebDefaultStylesheet', 'freeside4'); }, }, + TimeInICal => { + Section => 'General', + Overridable => 1, + SortOrder => 5, + Widget => '/Widgets/Form/Boolean', + WidgetArguments => { + Description => 'Include time in iCal feed events?', # loc + Hints => 'Formats iCal feed events with date and time' #loc + } + }, UseSideBySideLayout => { Section => 'Ticket composition', Overridable => 1, @@ -260,17 +305,6 @@ our %META = ( Description => 'Message box height', #loc }, }, - MessageBoxWrap => { - Section => 'Ticket composition', #loc - Overridable => 1, - SortOrder => 8.1, - Widget => '/Widgets/Form/Select', - WidgetArguments => { - Description => 'Message box wrapping', #loc - Values => [qw(SOFT HARD)], - Hints => "When the WYSIWYG editor is not enabled, this setting determines whether automatic line wraps in the ticket message box are sent to RT or not.", # loc - }, - }, DefaultTimeUnitsToHours => { Section => 'Ticket composition', #loc Overridable => 1, @@ -281,36 +315,52 @@ our %META = ( Hints => 'Only for entry, not display', #loc }, }, + RefreshIntervals => { + Type => 'ARRAY', + PostLoadCheck => sub { + my $self = shift; + my @intervals = $self->Get('RefreshIntervals'); + if (grep { $_ == 0 } @intervals) { + $RT::Logger->warning("Please do not include a 0 value in RefreshIntervals, as that default is already added for you."); + } + }, + }, SearchResultsRefreshInterval => { Section => 'General', #loc Overridable => 1, SortOrder => 9, Widget => '/Widgets/Form/Select', WidgetArguments => { - Description => 'Search results refresh interval', #loc - Values => [qw(0 120 300 600 1200 3600 7200)], - ValuesLabel => { - 0 => "Don't refresh search results.", #loc - 120 => "Refresh search results every 2 minutes.", #loc - 300 => "Refresh search results every 5 minutes.", #loc - 600 => "Refresh search results every 10 minutes.", #loc - 1200 => "Refresh search results every 20 minutes.", #loc - 3600 => "Refresh search results every 60 minutes.", #loc - 7200 => "Refresh search results every 120 minutes.", #loc - }, + Description => 'Search results refresh interval', #loc + Callback => sub { + my @values = RT->Config->Get('RefreshIntervals'); + my %labels = ( + 0 => "Don't refresh search results.", # loc + ); + + for my $value (@values) { + if ($value % 60 == 0) { + $labels{$value} = [ + 'Refresh search results every [quant,_1,minute,minutes].', #loc + $value / 60 + ]; + } + else { + $labels{$value} = [ + 'Refresh search results every [quant,_1,second,seconds].', #loc + $value + ]; + } + } + + unshift @values, 0; + + return { Values => \@values, ValuesLabel => \%labels }; + }, }, }, # User overridable options for RT at a glance - DefaultSummaryRows => { - Section => 'RT at a glance', #loc - Overridable => 1, - SortOrder => 1, - Widget => '/Widgets/Form/Integer', - WidgetArguments => { - Description => 'Number of search results', #loc - }, - }, HomePageRefreshInterval => { Section => 'RT at a glance', #loc Overridable => 1, @@ -318,20 +368,45 @@ our %META = ( Widget => '/Widgets/Form/Select', WidgetArguments => { Description => 'Home page refresh interval', #loc - Values => [qw(0 120 300 600 1200 3600 7200)], - ValuesLabel => { - 0 => "Don't refresh home page.", #loc - 120 => "Refresh home page every 2 minutes.", #loc - 300 => "Refresh home page every 5 minutes.", #loc - 600 => "Refresh home page every 10 minutes.", #loc - 1200 => "Refresh home page every 20 minutes.", #loc - 3600 => "Refresh home page every 60 minutes.", #loc - 7200 => "Refresh home page every 120 minutes.", #loc - }, + Callback => sub { + my @values = RT->Config->Get('RefreshIntervals'); + my %labels = ( + 0 => "Don't refresh home page.", # loc + ); + + for my $value (@values) { + if ($value % 60 == 0) { + $labels{$value} = [ + 'Refresh home page every [quant,_1,minute,minutes].', #loc + $value / 60 + ]; + } + else { + $labels{$value} = [ + 'Refresh home page every [quant,_1,second,seconds].', #loc + $value + ]; + } + } + + unshift @values, 0; + + return { Values => \@values, ValuesLabel => \%labels }; + }, }, }, # User overridable options for Ticket displays + PreferRichText => { + Section => 'Ticket display', # loc + Overridable => 1, + SortOrder => 0.9, + Widget => '/Widgets/Form/Boolean', + WidgetArguments => { + Description => 'Display messages in rich text if available', # loc + Hints => 'Rich text (HTML) shows formatting such as colored text, bold, italics, and more', # loc + }, + }, MaxInlineBody => { Section => 'Ticket display', #loc Overridable => 1, @@ -352,13 +427,19 @@ our %META = ( Description => 'Show oldest history first', #loc }, }, - DeferTransactionLoading => { + ShowHistory => { Section => 'Ticket display', Overridable => 1, SortOrder => 3, - Widget => '/Widgets/Form/Boolean', + Widget => '/Widgets/Form/Select', WidgetArguments => { - Description => 'Hide ticket history by default', #loc + Description => 'Show history', #loc + Values => [qw(delay click always)], + ValuesLabel => { + delay => "after the rest of the page loads", #loc + click => "after clicking a link", #loc + always => "immediately", #loc + }, }, }, ShowUnreadMessageNotifications => { @@ -372,13 +453,20 @@ our %META = ( }, PlainTextPre => { - Section => 'Ticket display', - Overridable => 1, - SortOrder => 5, - Widget => '/Widgets/Form/Boolean', - WidgetArguments => { - Description => 'add
 tag around plain text attachments', #loc
-            Hints       => "Use this to protect the format of plain text" #loc
+        PostSet => sub {
+            my $self  = shift;
+            my $value = shift;
+            $self->SetFromConfig(
+                Option => \'PlainTextMono',
+                Value  => [$value],
+                %{$self->Meta('PlainTextPre')->{'Source'}}
+            );
+        },
+        PostLoadCheck => sub {
+            my $self = shift;
+            # XXX: deprecated, remove in 4.4
+            $RT::Logger->info("You set \$PlainTextPre in your config, which has been removed in favor of \$PlainTextMono.  Please update your config.")
+                if $self->Meta('PlainTextPre')->{'Source'}{'Package'};
         },
     },
     PlainTextMono => {
@@ -387,18 +475,8 @@ our %META = (
         SortOrder       => 5,
         Widget          => '/Widgets/Form/Boolean',
         WidgetArguments => {
-            Description => 'display wrapped and formatted plain text attachments', #loc
-            Hints => 'Use css rules to display text monospaced and with formatting preserved, but wrap as needed.  This does not work well with IE6 and you should use the previous option', #loc
-        },
-    },
-    DisplayAfterQuickCreate => {
-        Section         => 'Ticket display',
-        Overridable     => 1,
-        SortOrder       => 6,
-        Widget          => '/Widgets/Form/Boolean',
-        WidgetArguments => {
-            Description => 'On Quick Create, redirect to ticket display', #loc
-            #Hints => '', #loc
+            Description => 'Display plain-text attachments in fixed-width font', #loc
+            Hints => 'Display all plain-text attachments in a monospace font with formatting preserved, but wrapping as needed.', #loc
         },
     },
     MoreAboutRequestorTicketList => {
@@ -407,11 +485,11 @@ our %META = (
         SortOrder       => 6,
         Widget          => '/Widgets/Form/Select',
         WidgetArguments => {
-            Description => q|What tickets to display in the 'More about requestor' box|,                #loc
+            Description => 'What tickets to display in the "More about requestor" box',                #loc
             Values      => [qw(Active Inactive All None)],
             ValuesLabel => {
-                Active   => "Show the Requestor's 10 highest priority open tickets",                  #loc
-                Inactive => "Show the Requestor's 10 highest priority closed tickets",      #loc
+                Active   => "Show the Requestor's 10 highest priority active tickets",                  #loc
+                Inactive => "Show the Requestor's 10 highest priority inactive tickets",      #loc
                 All      => "Show the Requestor's 10 highest priority tickets",      #loc
                 None     => "Show no tickets for the Requestor", #loc
             },
@@ -423,7 +501,7 @@ our %META = (
         SortOrder       => 7,
         Widget          => '/Widgets/Form/Boolean',
         WidgetArguments => {
-            Description => q|Show simplified recipient list on ticket update|,                #loc
+            Description => "Show simplified recipient list on ticket update",                #loc
         },
     },
     DisplayTicketAfterQuickCreate => {
@@ -432,9 +510,18 @@ our %META = (
         SortOrder       => 8,
         Widget          => '/Widgets/Form/Boolean',
         WidgetArguments => {
-            Description => q{Display ticket after "Quick Create"}, #loc
+            Description => 'Display ticket after "Quick Create"', #loc
         },
     },
+    QuoteFolding => {
+        Section => 'Ticket display',
+        Overridable => 1,
+        SortOrder => 9,
+        Widget => '/Widgets/Form/Boolean',
+        WidgetArguments => {
+            Description => 'Enable quote folding?' # loc
+        }
+    },
 
     # User overridable locale options
     DateTimeFormat => {
@@ -445,10 +532,13 @@ our %META = (
             Description => 'Date format',                            #loc
             Callback => sub { my $ret = { Values => [], ValuesLabel => {}};
                               my $date = RT::Date->new($HTML::Mason::Commands::session{'CurrentUser'});
-                              $date->Set;
+                              $date->SetToNow;
                               foreach my $value ($date->Formatters) {
                                  push @{$ret->{Values}}, $value;
-                                 $ret->{ValuesLabel}{$value} = $date->$value();
+                                 $ret->{ValuesLabel}{$value} = $date->Get(
+                                     Format     => $value,
+                                     Timezone   => 'user',
+                                 );
                               }
                               return $ret;
             },
@@ -465,7 +555,7 @@ our %META = (
                     'The RTAddressRegexp option is not set in the config.'
                     .' Not setting this option results in additional SQL queries to'
                     .' check whether each address belongs to RT or not.'
-                    .' It is especially important to set this option if RT recieves'
+                    .' It is especially important to set this option if RT receives'
                     .' emails on addresses that are not in the database or config.'
                 );
             } elsif (ref $value and ref $value eq "Regexp") {
@@ -518,6 +608,10 @@ our %META = (
     },
 
     # Internal config options
+    DatabaseExtraDSN => {
+        Type => 'HASH',
+    },
+
     FullTextSearch => {
         Type => 'HASH',
         PostLoadCheck => sub {
@@ -545,11 +639,26 @@ our %META = (
                     $RT::Logger->error("No Table set for full-text index; disabling");
                     $v->{Enable} = $v->{Indexed} = 0;
                 } elsif ($v->{'Table'} eq "Attachments") {
-                    $RT::Logger->error("Table for full-text index is set to Attachments, not SphinxSE table; disabling");
+                    $RT::Logger->error("Table for full-text index is set to Attachments, not FTS table; disabling");
                     $v->{Enable} = $v->{Indexed} = 0;
-                } elsif (not $v->{'MaxMatches'}) {
-                    $RT::Logger->warn("No MaxMatches set for full-text index; defaulting to 10000");
-                    $v->{MaxMatches} = 10_000;
+                } else {
+                    my (undef, $create) = eval { $RT::Handle->dbh->selectrow_array("SHOW CREATE TABLE " . $v->{Table}); };
+                    my ($engine) = ($create||'') =~ /engine=(\S+)/i;
+                    if (not $create) {
+                        $RT::Logger->error("External table ".$v->{Table}." does not exist");
+                        $v->{Enable} = $v->{Indexed} = 0;
+                    } elsif (lc $engine eq "sphinx") {
+                        # External Sphinx indexer
+                        $v->{Sphinx} = 1;
+                        unless ($v->{'MaxMatches'}) {
+                            $RT::Logger->warn("No MaxMatches set for full-text index; defaulting to 10000");
+                            $v->{MaxMatches} = 10_000;
+                        }
+                    } else {
+                        # Internal, one-column table
+                        $v->{Column} = 'Content';
+                        $v->{Engine} = $engine;
+                    }
                 }
             } else {
                 $RT::Logger->error("Indexed full-text-search not supported for $dbtype");
@@ -563,9 +672,7 @@ our %META = (
             my $self  = shift;
             my $value = shift;
             return if $value;
-            return if $INC{'GraphViz.pm'};
-            local $@;
-            return if eval {require GraphViz; 1};
+            return if GraphViz->require;
             $RT::Logger->debug("You've enabled GraphViz, but we couldn't load the module: $@");
             $self->Set( DisableGraphViz => 1 );
         },
@@ -576,59 +683,168 @@ our %META = (
             my $self  = shift;
             my $value = shift;
             return if $value;
-            return if $INC{'GD.pm'};
-            local $@;
-            return if eval {require GD; 1};
+            return if GD->require;
             $RT::Logger->debug("You've enabled GD, but we couldn't load the module: $@");
             $self->Set( DisableGD => 1 );
         },
     },
-    MailPlugins  => { Type => 'ARRAY' },
-    Plugins      => {
+    MailCommand => {
+        Type    => 'SCALAR',
+        PostLoadCheck => sub {
+            my $self = shift;
+            my $value = $self->Get('MailCommand');
+            return if ref($value) eq "CODE"
+                or $value =~/^(sendmail|sendmailpipe|qmail|testfile|mbox)$/;
+            $RT::Logger->error("Unknown value for \$MailCommand: $value; defaulting to sendmailpipe");
+            $self->Set( MailCommand => 'sendmailpipe' );
+        },
+    },
+    HTMLFormatter => {
+        Type => 'SCALAR',
+        PostLoadCheck => sub { RT::Interface::Email->_HTMLFormatter },
+    },
+    MailPlugins  => {
         Type => 'ARRAY',
         PostLoadCheck => sub {
             my $self = shift;
-            my $value = $self->Get('Plugins');
-            # XXX Remove in RT 4.2
-            return unless $value and grep {$_ eq "RT::FM"} @{$value};
-            warn 'RTFM has been integrated into core RT, and must be removed from your @Plugins';
+
+            # Make sure Crypt is post-loaded first
+            $META{Crypt}{'PostLoadCheck'}->( $self, $self->Get( 'Crypt' ) );
+
+            my @plugins = $self->Get('MailPlugins');
+            if ( grep $_ eq 'Auth::GnuPG' || $_ eq 'Auth::SMIME', @plugins ) {
+                $RT::Logger->warning(
+                    'Auth::GnuPG and Auth::SMIME (from an extension) have been'
+                    .' replaced with Auth::Crypt.  @MailPlugins has been adjusted,'
+                    .' but should be updated to replace both with Auth::Crypt to'
+                    .' silence this warning.'
+                );
+                my %seen;
+                @plugins =
+                    grep !$seen{$_}++,
+                    grep {
+                        $_ eq 'Auth::GnuPG' || $_ eq 'Auth::SMIME'
+                        ? 'Auth::Crypt' : $_
+                    } @plugins;
+                $self->Set( MailPlugins => @plugins );
+            }
+
+            if ( not @{$self->Get('Crypt')->{Incoming}} and grep $_ eq 'Auth::Crypt', @plugins ) {
+                $RT::Logger->warning("Auth::Crypt enabled in MailPlugins, but no available incoming encryption formats");
+            }
         },
     },
-    GnuPG        => { Type => 'HASH' },
-    GnuPGOptions => { Type => 'HASH',
+    Crypt        => {
+        Type => 'HASH',
+        PostLoadCheck => sub {
+            my $self = shift;
+            require RT::Crypt;
+
+            for my $proto (RT::Crypt->EnabledProtocols) {
+                my $opt = $self->Get($proto);
+                if (not RT::Crypt->LoadImplementation($proto)) {
+                    $RT::Logger->error("You enabled $proto, but we couldn't load module RT::Crypt::$proto");
+                    $opt->{'Enable'} = 0;
+                } elsif (not RT::Crypt->LoadImplementation($proto)->Probe) {
+                    $opt->{'Enable'} = 0;
+                } elsif ($META{$proto}{'PostLoadCheck'}) {
+                    $META{$proto}{'PostLoadCheck'}->( $self, $self->Get( $proto ) );
+                }
+
+            }
+
+            my $opt = $self->Get('Crypt');
+            my @enabled = RT::Crypt->EnabledProtocols;
+            my %enabled;
+            $enabled{$_} = 1 for @enabled;
+            $opt->{'Enable'} = scalar @enabled;
+            $opt->{'Incoming'} = [ $opt->{'Incoming'} ]
+                if $opt->{'Incoming'} and not ref $opt->{'Incoming'};
+            if ( $opt->{'Incoming'} && @{ $opt->{'Incoming'} } ) {
+                $RT::Logger->warning("$_ explicitly set as incoming Crypt plugin, but not marked Enabled; removing")
+                    for grep {not $enabled{$_}} @{$opt->{'Incoming'}};
+                $opt->{'Incoming'} = [ grep {$enabled{$_}} @{$opt->{'Incoming'}} ];
+            } else {
+                $opt->{'Incoming'} = \@enabled;
+            }
+            if ( $opt->{'Outgoing'} ) {
+                if (not $enabled{$opt->{'Outgoing'}}) {
+                    $RT::Logger->warning($opt->{'Outgoing'}.
+                                             " explicitly set as outgoing Crypt plugin, but not marked Enabled; "
+                                             . (@enabled ? "using $enabled[0]" : "removing"));
+                }
+                $opt->{'Outgoing'} = $enabled[0] unless $enabled{$opt->{'Outgoing'}};
+            } else {
+                $opt->{'Outgoing'} = $enabled[0];
+            }
+        },
+    },
+    SMIME        => {
+        Type => 'HASH',
+        PostLoadCheck => sub {
+            my $self = shift;
+            my $opt = $self->Get('SMIME');
+            return unless $opt->{'Enable'};
+
+            if (exists $opt->{Keyring}) {
+                unless ( File::Spec->file_name_is_absolute( $opt->{Keyring} ) ) {
+                    $opt->{Keyring} = File::Spec->catfile( $RT::BasePath, $opt->{Keyring} );
+                }
+                unless (-d $opt->{Keyring} and -r _) {
+                    $RT::Logger->info(
+                        "RT's SMIME libraries couldn't successfully read your".
+                        " configured SMIME keyring directory (".$opt->{Keyring}
+                        .").");
+                    delete $opt->{Keyring};
+                }
+            }
+
+            if (defined $opt->{CAPath}) {
+                if (-d $opt->{CAPath} and -r _) {
+                    # directory, all set
+                } elsif (-f $opt->{CAPath} and -r _) {
+                    # file, all set
+                } else {
+                    $RT::Logger->warn(
+                        "RT's SMIME libraries could not read your configured CAPath (".$opt->{CAPath}.")"
+                    );
+                    delete $opt->{CAPath};
+                }
+            }
+        },
+    },
+    GnuPG        => {
+        Type => 'HASH',
         PostLoadCheck => sub {
             my $self = shift;
             my $gpg = $self->Get('GnuPG');
             return unless $gpg->{'Enable'};
+
             my $gpgopts = $self->Get('GnuPGOptions');
+            unless ( File::Spec->file_name_is_absolute( $gpgopts->{homedir} ) ) {
+                $gpgopts->{homedir} = File::Spec->catfile( $RT::BasePath, $gpgopts->{homedir} );
+            }
             unless (-d $gpgopts->{homedir}  && -r _ ) { # no homedir, no gpg
-                $RT::Logger->debug(
+                $RT::Logger->info(
                     "RT's GnuPG libraries couldn't successfully read your".
                     " configured GnuPG home directory (".$gpgopts->{homedir}
-                    ."). PGP support has been disabled");
+                    ."). GnuPG support has been disabled");
                 $gpg->{'Enable'} = 0;
                 return;
             }
 
-
-            require RT::Crypt::GnuPG;
-            unless (RT::Crypt::GnuPG->Probe()) {
-                $RT::Logger->debug(
-                    "RT's GnuPG libraries couldn't successfully execute gpg.".
-                    " PGP support has been disabled");
-                $gpg->{'Enable'} = 0;
+            if ( grep exists $gpg->{$_}, qw(RejectOnMissingPrivateKey RejectOnBadData AllowEncryptDataInDB) ) {
+                $RT::Logger->warning(
+                    "The RejectOnMissingPrivateKey, RejectOnBadData and AllowEncryptDataInDB"
+                    ." GnuPG options are now properties of the generic Crypt configuration. You"
+                    ." should set them there instead."
+                );
+                delete $gpg->{$_} for qw(RejectOnMissingPrivateKey RejectOnBadData AllowEncryptDataInDB);
             }
         }
     },
-    ResolveDefaultUpdateType => {
-        PostLoadCheck => sub {
-            my $self  = shift;
-            my $value = shift;
-            return unless $value;
-            $RT::Logger->info('The ResolveDefaultUpdateType config option has been deprecated.  '.
-                              'You can change the site default in your %Lifecycles config.');
-        }
-    },
+    GnuPGOptions => { Type => 'HASH' },
+    ReferrerWhitelist => { Type => 'ARRAY' },
     WebPath => {
         PostLoadCheck => sub {
             my $self  = shift;
@@ -747,7 +963,7 @@ our %META = (
 
             my %seen;
             foreach my $encoding ( grep defined && length, splice @$value ) {
-                next if $seen{ $encoding }++;
+                next if $seen{ $encoding };
                 if ( $encoding eq '*' ) {
                     unshift @$value, '*';
                     next;
@@ -766,35 +982,88 @@ our %META = (
             }
         },
     },
+    LogToScreen => {
+        Deprecated => {
+            Instead => 'LogToSTDERR',
+            Remove  => '4.4',
+        },
+    },
+    UserAutocompleteFields => {
+        Deprecated => {
+            Instead => 'UserSearchFields',
+            Remove  => '4.4',
+        },
+    },
+    CustomFieldGroupings => {
+        Type            => 'HASH',
+        PostLoadCheck   => sub {
+            my $config = shift;
+            # use scalar context intentionally to avoid not a hash error
+            my $groups = $config->Get('CustomFieldGroupings') || {};
 
-    ActiveStatus => {
-        Type => 'ARRAY',
-        PostLoadCheck => sub {
-            my $self  = shift;
-            return unless shift;
-            # XXX Remove in RT 4.2
-            warn <Logger->error("Config option \%CustomFieldGroupings is a @{[ref $groups]} not a HASH; ignoring");
+                $groups = {};
+            }
+
+            for my $class (keys %$groups) {
+                my @h;
+                if (ref($groups->{$class}) eq 'HASH') {
+                    push @h, $_, $groups->{$class}->{$_}
+                        for sort {lc($a) cmp lc($b)} keys %{ $groups->{$class} };
+                } elsif (ref($groups->{$class}) eq 'ARRAY') {
+                    @h = @{ $groups->{$class} };
+                } else {
+                    RT->Logger->error("Config option \%CustomFieldGroupings{$class} is not a HASH or ARRAY; ignoring");
+                    delete $groups->{$class};
+                    next;
+                }
+
+                $groups->{$class} = [];
+                while (@h) {
+                    my $group = shift @h;
+                    my $ref   = shift @h;
+                    if (ref($ref) eq 'ARRAY') {
+                        push @{$groups->{$class}}, $group => $ref;
+                    } else {
+                        RT->Logger->error("Config option \%CustomFieldGroupings{$class}{$group} is not an ARRAY; ignoring");
+                    }
+                }
+            }
+            $config->Set( CustomFieldGroupings => %$groups );
         },
     },
-    InactiveStatus => {
-        Type => 'ARRAY',
-        PostLoadCheck => sub {
-            my $self  = shift;
-            return unless shift;
-            # XXX Remove in RT 4.2
-            warn < {
+        Type    => 'ARRAY',
+    },
+    WebExternalAuth           => { Deprecated => { Instead => 'WebRemoteUserAuth',             Remove => '4.4' }},
+    WebExternalAuthContinuous => { Deprecated => { Instead => 'WebRemoteUserContinuous',       Remove => '4.4' }},
+    WebFallbackToInternalAuth => { Deprecated => { Instead => 'WebFallbackToRTLogin',          Remove => '4.4' }},
+    WebExternalGecos          => { Deprecated => { Instead => 'WebRemoteUserGecos',            Remove => '4.4' }},
+    WebExternalAuto           => { Deprecated => { Instead => 'WebRemoteUserAutocreate',       Remove => '4.4' }},
+    AutoCreate                => { Deprecated => { Instead => 'UserAutocreateDefaultsOnLogin', Remove => '4.4' }},
+    LogoImageHeight => {
+        Deprecated => {
+            LogLevel => "info",
+            Message => "The LogoImageHeight configuration option did not affect display, and has been removed; please remove it from your RT_SiteConfig.pm",
+        },
+    },
+    LogoImageWidth => {
+        Deprecated => {
+            LogLevel => "info",
+            Message => "The LogoImageWidth configuration option did not affect display, and has been removed; please remove it from your RT_SiteConfig.pm",
+        },
+    },
+    DatabaseRequireSSL => {
+        Deprecated => {
+            Remove => '4.4',
+            LogLevel => "info",
+            Message => "The DatabaseRequireSSL configuration option did not enable SSL connections to the database, and has been removed; please remove it from your RT_SiteConfig.pm.  Use DatabaseExtraDSN to accomplish the same purpose.",
         },
     },
 );
 my %OPTIONS = ();
+my @LOADED_CONFIGS = ();
 
 =head1 METHODS
 
@@ -816,19 +1085,6 @@ sub _Init {
     return;
 }
 
-=head2 InitConfig
-
-Do nothin right now.
-
-=cut
-
-sub InitConfig {
-    my $self = shift;
-    my %args = ( File => '', @_ );
-    $args{'File'} =~ s/(?<=Config)(?=\.pm$)/Meta/;
-    return 1;
-}
-
 =head2 LoadConfigs
 
 Load all configs. First of all load RT's config then load
@@ -840,11 +1096,9 @@ Takes no arguments.
 sub LoadConfigs {
     my $self    = shift;
 
-    $self->InitConfig( File => 'RT_Config.pm' );
     $self->LoadConfig( File => 'RT_Config.pm' );
 
     my @configs = $self->Configs;
-    $self->InitConfig( File => $_ ) foreach @configs;
     $self->LoadConfig( File => $_ ) foreach @configs;
     return;
 }
@@ -872,9 +1126,13 @@ sub LoadConfig {
         and my $site_config = $ENV{RT_SITE_CONFIG} )
     {
         $self->_LoadConfig( %args, File => $site_config );
+        # to allow load siteconfig again and again in case it's updated
+        delete $INC{ $site_config };
     } else {
         $self->_LoadConfig(%args);
+        delete $INC{$args{'File'}};
     }
+
     $args{'File'} =~ s/Site(?=Config\.pm$)//;
     $self->_LoadConfig(%args);
     return 1;
@@ -907,6 +1165,20 @@ sub _LoadConfig {
                 Extension  => $is_ext,
             );
         };
+        local *Plugin = sub {
+            my (@new_plugins) = @_;
+            @new_plugins = map {s/-/::/g if not /:/; $_} @new_plugins;
+            my ( $pack, $file, $line ) = caller;
+            return $self->SetFromConfig(
+                Option     => \@RT::Plugins,
+                Value      => [@RT::Plugins, @new_plugins],
+                Package    => $pack,
+                File       => $file,
+                Line       => $line,
+                SiteConfig => $is_site,
+                Extension  => $is_ext,
+            );
+        };
         my @etc_dirs = ($RT::LocalEtcPath);
         push @etc_dirs, RT->PluginDirs('etc') if $is_ext;
         push @etc_dirs, $RT::EtcPath, @INC;
@@ -957,6 +1229,14 @@ EOF
         my $errormessage = sprintf( $message,
             $file_path, $fileusername, $filegroup, $filegroup );
         die "$errormessage\n$@";
+    } else {
+        # Loaded successfully
+        push @LOADED_CONFIGS, {
+            as          => $args{'File'},
+            filename    => $INC{ $args{'File'} },
+            extension   => $is_ext,
+            site        => $is_site,
+        };
     }
     return 1;
 }
@@ -993,6 +1273,40 @@ sub Configs {
     return @configs;
 }
 
+=head2 LoadedConfigs
+
+Returns a list of hashrefs, one for each config file loaded.  The keys of the
+hashes are:
+
+=over 4
+
+=item as
+
+Name this config file was loaded as (relative filename usually).
+
+=item filename
+
+The full path and filename.
+
+=item extension
+
+The "extension" part of the filename.  For example, the file C
+will have an C value of C.
+
+=item site
+
+True if the file is considered a site-level override.  For example, C
+will be false for C and true for C.
+
+=back
+
+=cut
+
+sub LoadedConfigs {
+    # Copy to avoid the caller changing our internal data
+    return map { \%$_ } @LOADED_CONFIGS
+}
+
 =head2 Get
 
 Takes name of the option as argument and returns its current value.
@@ -1028,7 +1342,6 @@ sub Get {
 
     my $res;
     if ( $user && $user->id && $META{$name}->{'Overridable'} ) {
-        $user = $user->UserObj if $user->isa('RT::CurrentUser');
         my $prefs = $user->Preferences($RT::System);
         $res = $prefs->{$name} if $prefs;
     }
@@ -1085,6 +1398,24 @@ sub Set {
         {no warnings 'once'; no strict 'refs'; ${"RT::$name"} = $OPTIONS{$name}; }
     }
     $META{$name}->{'Type'} = $type;
+    $META{$name}->{'PostSet'}->($self, $OPTIONS{$name}, $old)
+        if $META{$name}->{'PostSet'};
+    if ($META{$name}->{'Deprecated'}) {
+        my %deprecated = %{$META{$name}->{'Deprecated'}};
+        my $new_var = $deprecated{Instead} || '';
+        $self->SetFromConfig(
+            Option => \$new_var,
+            Value  => [$OPTIONS{$name}],
+            %{$self->Meta($name)->{'Source'}}
+        ) if $new_var;
+        $META{$name}->{'PostLoadCheck'} ||= sub {
+            RT->Deprecated(
+                Message => "Configuration option $name is deprecated",
+                Stack   => 0,
+                %deprecated,
+            );
+        };
+    }
     return $self->_ReturnValue( $old, $type );
 }
 
@@ -1120,7 +1451,7 @@ sub SetFromConfig {
     my $opt = $args{'Option'};
 
     my $type;
-    my $name = $self->__GetNameByRef($opt);
+    my $name = Symbol::Global::Name->find($opt);
     if ($name) {
         $type = ref $opt;
         $name =~ s/.*:://;
@@ -1180,67 +1511,6 @@ sub SetFromConfig {
     return 1;
 }
 
-    our %REF_SYMBOLS = (
-            SCALAR => '$',
-            ARRAY  => '@',
-            HASH   => '%',
-            CODE   => '&',
-        );
-
-{
-    my $last_pack = '';
-
-    sub __GetNameByRef {
-        my $self = shift;
-        my $ref  = shift;
-        my $pack = shift;
-        if ( !$pack && $last_pack ) {
-            my $tmp = $self->__GetNameByRef( $ref, $last_pack );
-            return $tmp if $tmp;
-        }
-        $pack ||= 'main::';
-        $pack .= '::' unless substr( $pack, -2 ) eq '::';
-
-        no strict 'refs';
-        my $name = undef;
-
-        # scan $pack's nametable(hash)
-        foreach my $k ( keys %{$pack} ) {
-
-            # The hash for main:: has a reference to itself
-            next if $k eq 'main::';
-
-            # if the entry has a trailing '::' then
-            # it is a link to another name space
-            if ( substr( $k, -2 ) eq '::') {
-                $name = $self->__GetNameByRef( $ref, $k );
-                return $name if $name;
-            }
-
-            # entry of the table with references to
-            # SCALAR, ARRAY... and other types with
-            # the same name
-            my $entry = ${$pack}{$k};
-            next unless $entry;
-
-            # get entry for type we are looking for
-            # XXX skip references to scalars or other references.
-            # Otherwie 5.10 goes boom. maybe we should skip any
-            # reference
-            next if ref($entry) eq 'SCALAR' || ref($entry) eq 'REF';
-            my $entry_ref = *{$entry}{ ref($ref) };
-            next unless $entry_ref;
-
-            # if references are equal then we've found
-            if ( $entry_ref == $ref ) {
-                $last_pack = $pack;
-                return ( $REF_SYMBOLS{ ref($ref) } || '*' ) . $pack . $k;
-            }
-        }
-        return '';
-    }
-}
-
 =head2 Metadata
 
 
@@ -1265,7 +1535,7 @@ sub Sections {
 sub Options {
     my $self = shift;
     my %args = ( Section => undef, Overridable => 1, Sorted => 1, @_ );
-    my @res  = keys %META;
+    my @res  = sort keys %META;
     
     @res = grep( ( $META{$_}->{'Section'} || 'General' ) eq $args{'Section'},
         @res