1# BEGIN BPS TAGGED BLOCK {{{
2#
3# COPYRIGHT:
4#
5# This software is Copyright (c) 1996-2021 Best Practical Solutions, LLC
6#                                          <sales@bestpractical.com>
7#
8# (Except where explicitly superseded by other copyright notices)
9#
10#
11# LICENSE:
12#
13# This work is made available to you under the terms of Version 2 of
14# the GNU General Public License. A copy of that license should have
15# been provided with this software, but in any event can be snarfed
16# from www.gnu.org.
17#
18# This work is distributed in the hope that it will be useful, but
19# WITHOUT ANY WARRANTY; without even the implied warranty of
20# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21# General Public License for more details.
22#
23# You should have received a copy of the GNU General Public License
24# along with this program; if not, write to the Free Software
25# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26# 02110-1301 or visit their web page on the internet at
27# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28#
29#
30# CONTRIBUTION SUBMISSION POLICY:
31#
32# (The following paragraph is not intended to limit the rights granted
33# to you to modify and distribute this software under the terms of
34# the GNU General Public License and is only of importance to you if
35# you choose to contribute your changes and enhancements to the
36# community by submitting them to Best Practical Solutions, LLC.)
37#
38# By intentionally submitting any modifications, corrections or
39# derivatives to this work, or any other work intended for use with
40# Request Tracker, to Best Practical Solutions, LLC, you confirm that
41# you are the copyright holder for those contributions and you grant
42# Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43# royalty-free, perpetual, license to use, copy, create derivative
44# works based on those contributions, and sublicense and distribute
45# those contributions and any derivatives thereof.
46#
47# END BPS TAGGED BLOCK }}}
48
49package RT::Config;
50
51use strict;
52use warnings;
53
54use 5.010;
55use File::Spec ();
56use Symbol::Global::Name;
57use List::MoreUtils 'uniq';
58use Clone ();
59
60# Store log messages generated before RT::Logger is available
61our @PreInitLoggerMessages;
62
63=head1 NAME
64
65    RT::Config - RT's config
66
67=head1 SYNOPSYS
68
69    # get config object
70    use RT::Config;
71    my $config = RT::Config->new;
72    $config->LoadConfigs;
73
74    # get or set option
75    my $rt_web_path = $config->Get('WebPath');
76    $config->Set(EmailOutputEncoding => 'latin1');
77
78    # get config object from RT package
79    use RT;
80    RT->LoadConfig;
81    my $config = RT->Config;
82
83=head1 DESCRIPTION
84
85C<RT::Config> class provide access to RT's and RT extensions' config files.
86
87RT uses two files for site configuring:
88
89First file is F<RT_Config.pm> - core config file. This file is shipped
90with RT distribution and contains default values for all available options.
91B<You should never edit this file.>
92
93Second file is F<RT_SiteConfig.pm> - site config file. You can use it
94to customize your RT instance. In this file you can override any option
95listed in core config file.
96
97You may also split settings into separate files under the
98F<etc/RT_SiteConfig.d/> directory.  All files ending in C<.pm> will be parsed,
99in alphabetical order, after F<RT_SiteConfig.pm> is loaded.
100
101RT extensions could also provide their config files. Extensions should
102use F<< <NAME>_Config.pm >> and F<< <NAME>_SiteConfig.pm >> names for
103config files, where <NAME> is extension name.
104
105B<NOTE>: All options from RT's config and extensions' configs are saved
106in one place and thus extension could override RT's options, but it is not
107recommended.
108
109=cut
110
111=head2 %META
112
113Hash of Config options that may be user overridable
114or may require more logic than should live in RT_*Config.pm
115
116Keyed by config name, there are several properties that
117can be set for each config optin:
118
119 Section     - What header this option should be grouped
120               under on the user Preferences page
121 Overridable - Can users change this option
122 SortOrder   - Within a Section, how should the options be sorted
123               for display to the user
124 Widget      - Mason component path to widget that should be used
125               to display this config option
126 WidgetArguments - An argument hash passed to the WIdget
127    Description - Friendly description to show the user
128    Values      - Arrayref of options (for select Widget)
129    ValuesLabel - Hashref, key is the Value from the Values
130                  list, value is a user friendly description
131                  of the value
132    Callback    - subref that receives no arguments.  It returns
133                  a hashref of items that are added to the rest
134                  of the WidgetArguments
135 PostSet       - subref passed the RT::Config object and the current and
136                 previous setting of the config option.  This is called well
137                 before much of RT's subsystems are initialized, so what you
138                 can do here is pretty limited.  It's mostly useful for
139                 effecting the value of other config options early.
140 PostLoadCheck - subref passed the RT::Config object and the current
141                 setting of the config option.  Can make further checks
142                 (such as seeing if a library is installed) and then change
143                 the setting of this or other options in the Config using
144                 the RT::Config option.
145   Obfuscate   - subref passed the RT::Config object, current setting of the config option
146                 and a user object, can return obfuscated value. it's called in
147                 RT->Config->GetObfuscated()
148
149=cut
150
151our %META;
152%META = (
153    # General user overridable options
154    RestrictReferrerLogin => {
155        PostLoadCheck => sub {
156            my $self = shift;
157            if (defined($self->Get('RestrictReferrerLogin'))) {
158                RT::Logger->error("The config option 'RestrictReferrerLogin' is incorrect, and should be 'RestrictLoginReferrer' instead.");
159            }
160        },
161    },
162    DefaultQueue => {
163        Section         => 'General',
164        Overridable     => 1,
165        SortOrder       => 1,
166        Widget          => '/Widgets/Form/Select',
167        WidgetArguments => {
168            Description => 'Default queue',    #loc
169            Default     => 1, # allow user to unset it on EditConfig.html
170            Callback    => sub {
171                my $ret = { Values => [], ValuesLabel => {}};
172                my $q = RT::Queues->new($HTML::Mason::Commands::session{'CurrentUser'});
173                $q->UnLimit;
174                while (my $queue = $q->Next) {
175                    next unless $queue->CurrentUserHasRight("CreateTicket");
176                    push @{$ret->{Values}}, $queue->Id;
177                    $ret->{ValuesLabel}{$queue->Id} = $queue->Name;
178                }
179                return $ret;
180            },
181        }
182    },
183    RememberDefaultQueue => {
184        Section     => 'General',
185        Overridable => 1,
186        SortOrder   => 2,
187        Widget      => '/Widgets/Form/Boolean',
188        WidgetArguments => {
189            Description => 'Remember default queue' # loc
190        }
191    },
192    UsernameFormat => {
193        Section         => 'General',
194        Overridable     => 1,
195        SortOrder       => 3,
196        Widget          => '/Widgets/Form/Select',
197        WidgetArguments => {
198            Description => 'Username format', # loc
199            Values      => [qw(role concise verbose)],
200            ValuesLabel => {
201                role    => 'Privileged: usernames; Unprivileged: names and email addresses', # loc
202                concise => 'Short usernames', # loc
203                verbose => 'Name and email address', # loc
204            },
205        },
206    },
207    AutocompleteOwners => {
208        Section     => 'General',
209        Overridable => 1,
210        SortOrder   => 3.1,
211        Widget      => '/Widgets/Form/Boolean',
212        WidgetArguments => {
213            Description => 'Use autocomplete to find owners?', # loc
214            Hints       => 'Replaces the owner dropdowns with textboxes' #loc
215        }
216    },
217    AutocompleteQueues => {
218        Section     => 'General',
219        Overridable => 1,
220        SortOrder   => 3.2,
221        Widget      => '/Widgets/Form/Boolean',
222        WidgetArguments => {
223            Description => 'Use autocomplete to find queues?', # loc
224            Hints       => 'Replaces the queue dropdowns with textboxes' #loc
225        }
226    },
227    WebDefaultStylesheet => {
228        Section         => 'General',                #loc
229        Overridable     => 1,
230        SortOrder       => 4,
231        Widget          => '/Widgets/Form/Select',
232        WidgetArguments => {
233            Description => 'Theme',                  #loc
234            Callback    => sub {
235                state @stylesheets;
236                unless (@stylesheets) {
237                    for my $static_path ( RT::Interface::Web->StaticRoots ) {
238                        my $css_path =
239                          File::Spec->catdir( $static_path, 'css' );
240                        next unless -d $css_path;
241                        if ( opendir my $dh, $css_path ) {
242                            push @stylesheets, grep {
243                                -e File::Spec->catfile( $css_path, $_, 'main.css' )
244                            } readdir $dh;
245                        }
246                        else {
247                            RT->Logger->error("Can't read $css_path: $!");
248                        }
249                    }
250                    @stylesheets = sort { lc $a cmp lc $b } uniq @stylesheets;
251                }
252                return { Values => \@stylesheets };
253            },
254        },
255        PostLoadCheck => sub {
256            my $self = shift;
257            my $value = $self->Get('WebDefaultStylesheet');
258
259            my @roots = RT::Interface::Web->StaticRoots;
260            for my $root (@roots) {
261                return if -d "$root/css/$value";
262            }
263
264            $RT::Logger->warning(
265                "The default stylesheet ($value) does not exist in this instance of RT. "
266              . "Defaulting to elevator-light."
267            );
268
269            $self->Set('WebDefaultStylesheet', 'elevator-light');
270        },
271    },
272    TimeInICal => {
273        Section     => 'General',
274        Overridable => 1,
275        SortOrder   => 5,
276        Widget      => '/Widgets/Form/Boolean',
277        WidgetArguments => {
278            Description => 'Include time in iCal feed events?', # loc
279            Hints       => 'Formats iCal feed events with date and time' #loc
280        }
281    },
282    UseSideBySideLayout => {
283        Section => 'Ticket composition',
284        Overridable => 1,
285        SortOrder => 5,
286        Widget => '/Widgets/Form/Boolean',
287        WidgetArguments => {
288            Description => 'Use a two column layout for create and update forms?' # loc
289        }
290    },
291    MessageBoxRichText => {
292        Section => 'Ticket composition',
293        Overridable => 1,
294        SortOrder => 5.1,
295        Widget => '/Widgets/Form/Boolean',
296        WidgetArguments => {
297            Description => 'WYSIWYG message composer' # loc
298        }
299    },
300    MessageBoxRichTextHeight => {
301        Section => 'Ticket composition',
302        Overridable => 1,
303        SortOrder => 6,
304        Widget => '/Widgets/Form/Integer',
305        WidgetArguments => {
306            Description => 'WYSIWYG composer height', # loc
307        }
308    },
309    MessageBoxWidth => {
310        Section         => 'Ticket composition',
311        Overridable     => 1,
312        SortOrder       => 7,
313        Widget          => '/Widgets/Form/Integer',
314        WidgetArguments => {
315            Description => 'Message box width',           #loc
316        },
317    },
318    MessageBoxHeight => {
319        Section         => 'Ticket composition',
320        Overridable     => 1,
321        SortOrder       => 8,
322        Widget          => '/Widgets/Form/Integer',
323        WidgetArguments => {
324            Description => 'Message box height',          #loc
325        },
326    },
327    DefaultTimeUnitsToHours => {
328        Section         => 'Ticket composition', #loc
329        Overridable     => 1,
330        SortOrder       => 9,
331        Widget          => '/Widgets/Form/Boolean',
332        WidgetArguments => {
333            Description => 'Enter time in hours by default', #loc
334            Hints       => 'Only for entry, not display', #loc
335        },
336    },
337    SignatureAboveQuote => {
338        Section         => 'Ticket composition', #loc
339        Overridable     => 1,
340        SortOrder       => 10,
341        Widget          => '/Widgets/Form/Boolean',
342        WidgetArguments => {
343            Description => 'Place signature above quote', #loc
344        },
345    },
346    PreferDropzone => {
347        Section         => 'Ticket composition', #loc
348        Overridable     => 1,
349        SortOrder       => 11,
350        Widget          => '/Widgets/Form/Boolean',
351        WidgetArguments => {
352            Description => 'Use dropzone if available', #loc
353        },
354    },
355    RefreshIntervals => {
356        Type => 'ARRAY',
357        PostLoadCheck => sub {
358            my $self = shift;
359            my @intervals = $self->Get('RefreshIntervals');
360            if (grep { $_ == 0 } @intervals) {
361                $RT::Logger->warning("Please do not include a 0 value in RefreshIntervals, as that default is already added for you.");
362            }
363        },
364    },
365    SearchResultsRefreshInterval => {
366        Section         => 'General',                       #loc
367        Overridable     => 1,
368        SortOrder       => 9,
369        Widget          => '/Widgets/Form/Select',
370        WidgetArguments => {
371            Description => 'Search results refresh interval', #loc
372            Callback    => sub {
373                my @values = RT->Config->Get('RefreshIntervals');
374                my %labels = (
375                    0 => "Don't refresh search results.", # loc
376                );
377
378                for my $value (@values) {
379                    if ($value % 60 == 0) {
380                        $labels{$value} = [
381                            'Refresh search results every [quant,_1,minute,minutes].', #loc
382                            $value / 60
383                        ];
384                    }
385                    else {
386                        $labels{$value} = [
387                            'Refresh search results every [quant,_1,second,seconds].', #loc
388                            $value
389                        ];
390                    }
391                }
392
393                unshift @values, 0;
394
395                return { Values => \@values, ValuesLabel => \%labels };
396            },
397        },
398    },
399    EnableJSChart => {
400        Section         => 'General',                       #loc
401        Overridable     => 1,
402        SortOrder       => 10,
403        Widget          => '/Widgets/Form/Boolean',
404        WidgetArguments => {
405            Description => 'Use JavaScript to render charts', #loc
406        },
407    },
408    JSChartColorScheme => {
409        Section         => 'General',                       #loc
410        Overridable     => 1,
411        SortOrder       => 11,
412        Widget          => '/Widgets/Form/String',
413        WidgetArguments => {
414            Description => 'JavaScript chart color scheme', #loc
415        },
416    },
417
418    # User overridable options for RT at a glance
419    HomePageRefreshInterval => {
420        Section         => 'RT at a glance',                       #loc
421        Overridable     => 1,
422        SortOrder       => 2,
423        Widget          => '/Widgets/Form/Select',
424        WidgetArguments => {
425            Description => 'Home page refresh interval',                #loc
426            Callback    => sub {
427                my @values = RT->Config->Get('RefreshIntervals');
428                my %labels = (
429                    0 => "Don't refresh home page.", # loc
430                );
431
432                for my $value (@values) {
433                    if ($value % 60 == 0) {
434                        $labels{$value} = [
435                            'Refresh home page every [quant,_1,minute,minutes].', #loc
436                            $value / 60
437                        ];
438                    }
439                    else {
440                        $labels{$value} = [
441                            'Refresh home page every [quant,_1,second,seconds].', #loc
442                            $value
443                        ];
444                    }
445                }
446
447                unshift @values, 0;
448
449                return { Values => \@values, ValuesLabel => \%labels };
450            },
451        },
452    },
453
454    # User overridable options for Ticket displays
455    PreferRichText => {
456        Section         => 'Ticket display', # loc
457        Overridable     => 1,
458        SortOrder       => 0.9,
459        Widget          => '/Widgets/Form/Boolean',
460        WidgetArguments => {
461            Description => 'Display messages in rich text if available', # loc
462            Hints       => 'Rich text (HTML) shows formatting such as colored text, bold, italics, and more', # loc
463        },
464    },
465    MaxInlineBody => {
466        Section         => 'Ticket display',              #loc
467        Overridable     => 1,
468        SortOrder       => 1,
469        Widget          => '/Widgets/Form/Integer',
470        WidgetArguments => {
471            Description => 'Maximum inline message length',    #loc
472            Hints =>
473            "Length in characters; Use '0' to show all messages inline, regardless of length" #loc
474        },
475    },
476    OldestTransactionsFirst => {
477        Section         => 'Ticket display',
478        Overridable     => 1,
479        SortOrder       => 2,
480        Widget          => '/Widgets/Form/Boolean',
481        WidgetArguments => {
482            Description => 'Show oldest history first',    #loc
483        },
484    },
485    ShowHistory => {
486        Section         => 'Ticket display',
487        Overridable     => 1,
488        SortOrder       => 3,
489        Widget          => '/Widgets/Form/Select',
490        WidgetArguments => {
491            Description => 'Show history',                #loc
492            Values      => [qw(delay click always scroll)],
493            ValuesLabel => {
494                delay   => "after the rest of the page loads",  #loc
495                click   => "after clicking a link",             #loc
496                always  => "immediately",                       #loc
497                scroll  => "as you scroll",                     #loc
498            },
499        },
500    },
501    ShowUnreadMessageNotifications => {
502        Section         => 'Ticket display',
503        Overridable     => 1,
504        SortOrder       => 4,
505        Widget          => '/Widgets/Form/Boolean',
506        WidgetArguments => {
507            Description => 'Notify me of unread messages',    #loc
508        },
509
510    },
511    PlainTextMono => {
512        Section         => 'Ticket display',
513        Overridable     => 1,
514        SortOrder       => 5,
515        Widget          => '/Widgets/Form/Boolean',
516        WidgetArguments => {
517            Description => 'Display plain-text attachments in fixed-width font', #loc
518            Hints => 'Display all plain-text attachments in a monospace font with formatting preserved, but wrapping as needed.', #loc
519        },
520    },
521    MoreAboutRequestorTicketList => {
522        Section         => 'Ticket display',                       #loc
523        Overridable     => 1,
524        SortOrder       => 6,
525        Widget          => '/Widgets/Form/Select',
526        WidgetArguments => {
527            Description => 'What tickets to display in the "More about requestor" box',                #loc
528            Values      => [qw(Active Inactive All None)],
529            ValuesLabel => {
530                Active   => "Show the Requestor's 10 highest priority active tickets",                  #loc
531                Inactive => "Show the Requestor's 10 highest priority inactive tickets",      #loc
532                All      => "Show the Requestor's 10 highest priority tickets",      #loc
533                None     => "Show no tickets for the Requestor", #loc
534            },
535        },
536    },
537    SimplifiedRecipients => {
538        Section         => 'Ticket display',                       #loc
539        Overridable     => 1,
540        SortOrder       => 7,
541        Widget          => '/Widgets/Form/Boolean',
542        WidgetArguments => {
543            Description => "Show simplified recipient list on ticket update",                #loc
544        },
545    },
546    SquelchedRecipients => {
547        Section         => 'Ticket display',                       #loc
548        Overridable     => 1,
549        SortOrder       => 8,
550        Widget          => '/Widgets/Form/Boolean',
551        WidgetArguments => {
552            Description => "Default to squelching all outgoing email notifications (from web interface) on ticket update", #loc
553        },
554    },
555    DisplayTicketAfterQuickCreate => {
556        Section         => 'Ticket display',
557        Overridable     => 1,
558        SortOrder       => 9,
559        Widget          => '/Widgets/Form/Boolean',
560        WidgetArguments => {
561            Description => 'Display ticket after "Quick Create"', #loc
562        },
563    },
564    QuoteFolding => {
565        Section => 'Ticket display',
566        Overridable => 1,
567        SortOrder => 10,
568        Widget => '/Widgets/Form/Boolean',
569        WidgetArguments => {
570            Description => 'Enable quote folding?' # loc
571        }
572    },
573    HideUnsetFieldsOnDisplay => {
574        Section => 'Ticket display',
575        Overridable => 1,
576        SortOrder => 11,
577        Widget => '/Widgets/Form/Boolean',
578        WidgetArguments => {
579            Description => 'Hide unset fields?' # loc
580        }
581    },
582    InlineEdit => {
583        Section => 'Ticket display',
584        Overridable => 1,
585        SortOrder => 12,
586        Widget => '/Widgets/Form/Boolean',
587        WidgetArguments => {
588            Description => 'Enable inline edit?' # loc
589        }
590    },
591
592    InlineEditPanelBehavior => {
593        Type            => 'HASH',
594        PostLoadCheck   => sub {
595            my $config = shift;
596            # use scalar context intentionally to avoid not a hash error
597            my $behavior = $config->Get('InlineEditPanelBehavior') || {};
598
599            unless (ref($behavior) eq 'HASH') {
600                RT->Logger->error("Config option \%InlineEditPanelBehavior is a @{[ref $behavior]} not a HASH; ignoring");
601                $behavior = {};
602            }
603
604            my %valid = map { $_ => 1 } qw/link click always hide/;
605            for my $class (keys %$behavior) {
606                if (ref($behavior->{$class}) eq 'HASH') {
607                    for my $panel (keys %{ $behavior->{$class} }) {
608                        my $value = $behavior->{$class}{$panel};
609                        if (!$valid{$value}) {
610                            RT->Logger->error("Config option \%InlineEditPanelBehavior{$class}{$panel}, which is '$value', must be one of: " . (join ', ', map { "'$_'" } sort keys %valid) . "; ignoring");
611                            delete $behavior->{$class}{$panel};
612                        }
613                    }
614                } else {
615                    RT->Logger->error("Config option \%InlineEditPanelBehavior{$class} is not a HASH; ignoring");
616                    delete $behavior->{$class};
617                    next;
618                }
619            }
620
621            $config->Set( InlineEditPanelBehavior => %$behavior );
622        },
623    },
624    ShowSearchNavigation => {
625        Section     => 'Ticket display',
626        Overridable => 1,
627        SortOrder   => 13,
628        Widget      => '/Widgets/Form/Boolean',
629        WidgetArguments => {
630            Description => 'Show search navigation', # loc
631            Hints       => 'Show search navigation links of "First", "Last", "Prev" and "Next"', # loc
632        }
633    },
634
635    # User overridable locale options
636    DateTimeFormat => {
637        Section         => 'Locale',                       #loc
638        Overridable     => 1,
639        Widget          => '/Widgets/Form/Select',
640        WidgetArguments => {
641            Description => 'Date format',                            #loc
642            Callback => sub { my $ret = { Values => [], ValuesLabel => {}};
643                              my $date = RT::Date->new($HTML::Mason::Commands::session{'CurrentUser'});
644                              $date->SetToNow;
645                              foreach my $value ($date->Formatters) {
646                                 push @{$ret->{Values}}, $value;
647                                 $ret->{ValuesLabel}{$value} = $date->Get(
648                                     Format     => $value,
649                                     Timezone   => 'user',
650                                 );
651                              }
652                              return $ret;
653            },
654        },
655    },
656
657    RTAddressRegexp => {
658        Type    => 'SCALAR',
659        Immutable => 1,
660        PostLoadCheck => sub {
661            my $self = shift;
662            my $value = $self->Get('RTAddressRegexp');
663            if (not $value) {
664                $RT::Logger->debug(
665                    'The RTAddressRegexp option is not set in the config.'
666                    .' Not setting this option results in additional SQL queries to'
667                    .' check whether each address belongs to RT or not.'
668                    .' It is especially important to set this option if RT receives'
669                    .' emails on addresses that are not in the database or config.'
670                );
671            } elsif (ref $value and ref $value eq "Regexp") {
672                # Ensure that the regex is case-insensitive; while the
673                # local part of email addresses is _technically_
674                # case-sensitive, most MTAs don't treat it as such.
675                $RT::Logger->warning(
676                    'RTAddressRegexp is set to a case-sensitive regular expression.'
677                    .' This may lead to mail loops with MTAs which treat the'
678                    .' local part as case-insensitive -- which is most of them.'
679                ) if "$value" =~ /^\(\?[a-z]*-([a-z]*):/ and "$1" =~ /i/;
680            }
681        },
682    },
683    # User overridable mail options
684    EmailFrequency => {
685        Section         => 'Mail',                                     #loc
686        Overridable     => 1,
687        Default     => 'Individual messages',
688        Widget          => '/Widgets/Form/Select',
689        WidgetArguments => {
690            Description => 'Email delivery',    #loc
691            Values      => [
692            'Individual messages',    #loc
693            'Daily digest',           #loc
694            'Weekly digest',          #loc
695            'Suspended'               #loc
696            ]
697        }
698    },
699    NotifyActor => {
700        Section         => 'Mail',                                     #loc
701        Overridable     => 1,
702        SortOrder       => 2,
703        Widget          => '/Widgets/Form/Boolean',
704        WidgetArguments => {
705            Description => 'Outgoing mail', #loc
706            Hints => 'Should RT send you mail for ticket updates you make?', #loc
707        }
708    },
709
710    # this tends to break extensions that stash links in ticket update pages
711    Organization => {
712        Type            => 'SCALAR',
713        Immutable       => 1,
714        Widget          => '/Widgets/Form/String',
715        PostLoadCheck   => sub {
716            my ($self,$value) = @_;
717            $RT::Logger->error("your \$Organization setting ($value) appears to contain whitespace.  Please fix this.")
718                if $value =~ /\s/;;
719        },
720    },
721
722    rtname => {
723        Immutable => 1,
724        Widget    => '/Widgets/Form/String',
725    },
726
727    # Internal config options
728    DatabaseExtraDSN => {
729        Type      => 'HASH',
730        Immutable => 1,
731    },
732    DatabaseAdmin => {
733        Immutable => 1,
734        Widget    => '/Widgets/Form/String',
735    },
736    DatabaseHost => {
737        Immutable => 1,
738        Widget    => '/Widgets/Form/String',
739    },
740    DatabaseName => {
741        Immutable => 1,
742        Widget    => '/Widgets/Form/String',
743    },
744    DatabasePassword => {
745        Immutable => 1,
746        Widget    => '/Widgets/Form/String',
747        Obfuscate => sub {
748            my ($config, $sources, $user) = @_;
749            return $user->loc('Password not printed');
750        },
751    },
752    DatabasePort => {
753        Immutable => 1,
754        Widget    => '/Widgets/Form/Integer',
755    },
756    DatabaseRTHost => {
757        Immutable => 1,
758        Widget    => '/Widgets/Form/String',
759    },
760    DatabaseType => {
761        Immutable => 1,
762        Widget    => '/Widgets/Form/String',
763    },
764    DatabaseUser => {
765        Immutable => 1,
766        Widget    => '/Widgets/Form/String',
767    },
768
769    FullTextSearch => {
770        Type => 'HASH',
771        PostLoadCheck => sub {
772            my $self = shift;
773            my $v = $self->Get('FullTextSearch');
774            return unless $v->{Enable} and $v->{Indexed};
775            my $dbtype = $self->Get('DatabaseType');
776            if ($dbtype eq 'Oracle') {
777                if (not $v->{IndexName}) {
778                    $RT::Logger->error("No IndexName set for full-text index; disabling");
779                    $v->{Enable} = $v->{Indexed} = 0;
780                }
781            } elsif ($dbtype eq 'Pg') {
782                my $bad = 0;
783                if (not $v->{'Column'}) {
784                    $RT::Logger->error("No Column set for full-text index; disabling");
785                    $v->{Enable} = $v->{Indexed} = 0;
786                } elsif ($v->{'Column'} eq "Content"
787                             and (not $v->{'Table'} or $v->{'Table'} eq "Attachments")) {
788                    $RT::Logger->error("Column for full-text index is set to Content, not tsvector column; disabling");
789                    $v->{Enable} = $v->{Indexed} = 0;
790                }
791            } elsif ($dbtype eq 'mysql') {
792                if (not $v->{'Table'}) {
793                    $RT::Logger->error("No Table set for full-text index; disabling");
794                    $v->{Enable} = $v->{Indexed} = 0;
795                } elsif ($v->{'Table'} eq "Attachments") {
796                    $RT::Logger->error("Table for full-text index is set to Attachments, not FTS table; disabling");
797                    $v->{Enable} = $v->{Indexed} = 0;
798                } else {
799                    my (undef, $create) = eval { $RT::Handle->dbh->selectrow_array("SHOW CREATE TABLE " . $v->{Table}); };
800                    my ($engine) = ($create||'') =~ /engine=(\S+)/i;
801                    if (not $create) {
802                        $RT::Logger->error("External table ".$v->{Table}." does not exist");
803                        $v->{Enable} = $v->{Indexed} = 0;
804                    } elsif (lc $engine eq "sphinx") {
805                        # External Sphinx indexer
806                        $v->{Sphinx} = 1;
807                        unless ($v->{'MaxMatches'}) {
808                            $RT::Logger->warn("No MaxMatches set for full-text index; defaulting to 10000");
809                            $v->{MaxMatches} = 10_000;
810                        }
811                    } else {
812                        # Internal, one-column table
813                        $v->{Column} = 'Content';
814                        $v->{Engine} = $engine;
815                    }
816                }
817            } else {
818                $RT::Logger->error("Indexed full-text-search not supported for $dbtype");
819                $v->{Indexed} = 0;
820            }
821        },
822    },
823    DisableGraphViz => {
824        Type            => 'SCALAR',
825        Widget          => '/Widgets/Form/Boolean',
826        PostLoadCheck   => sub {
827            my $self  = shift;
828            my $value = shift;
829            return if $value;
830            return if GraphViz->require;
831            $RT::Logger->debug("You've enabled GraphViz, but we couldn't load the module: $@");
832            $self->Set( DisableGraphViz => 1 );
833        },
834    },
835    DisableGD => {
836        Type            => 'SCALAR',
837        Widget          => '/Widgets/Form/Boolean',
838        PostLoadCheck   => sub {
839            my $self  = shift;
840            my $value = shift;
841            return if $value;
842            return if GD->require;
843            $RT::Logger->debug("You've enabled GD, but we couldn't load the module: $@");
844            $self->Set( DisableGD => 1 );
845        },
846    },
847    MailCommand => {
848        Type    => 'SCALAR',
849        Widget  => '/Widgets/Form/String',
850        PostLoadCheck => sub {
851            my $self = shift;
852            my $value = $self->Get('MailCommand');
853            return if ref($value) eq "CODE"
854                or $value =~/^(sendmail|sendmailpipe|qmail|testfile|mbox)$/;
855            $RT::Logger->error("Unknown value for \$MailCommand: $value; defaulting to sendmailpipe");
856            $self->Set( MailCommand => 'sendmailpipe' );
857        },
858    },
859    HTMLFormatter => {
860        Type => 'SCALAR',
861        Widget => '/Widgets/Form/String',
862        PostLoadCheck => sub { RT::Interface::Email->_HTMLFormatter },
863    },
864    Plugins => {
865        Immutable => 1,
866    },
867    RecordBaseClass => {
868        Immutable => 1,
869        Widget    => '/Widgets/Form/String',
870    },
871    WebSessionClass => {
872        Immutable => 1,
873        Widget    => '/Widgets/Form/String',
874    },
875    DevelMode => {
876        Immutable => 1,
877        Widget    => '/Widgets/Form/Boolean',
878    },
879    DisallowExecuteCode => {
880        Immutable => 1,
881        Widget    => '/Widgets/Form/Boolean',
882    },
883    MailPlugins  => {
884        Type => 'ARRAY',
885        Immutable     => 1,
886        PostLoadCheck => sub {
887            my $self = shift;
888
889            # Make sure Crypt is post-loaded first
890            $META{Crypt}{'PostLoadCheck'}->( $self, $self->Get( 'Crypt' ) );
891
892            RT::Interface::Email::Plugins(Add => ["Authz::Default", "Action::Defaults"]);
893            RT::Interface::Email::Plugins(Add => ["Auth::MailFrom"])
894                  unless RT::Interface::Email::Plugins(Code => 1, Method => "GetCurrentUser");
895        },
896    },
897    Crypt        => {
898        Immutable => 1,
899        Invisible => 1,
900        Type => 'HASH',
901        PostLoadCheck => sub {
902            my $self = shift;
903            require RT::Crypt;
904
905            for my $proto (RT::Crypt->EnabledProtocols) {
906                my $opt = $self->Get($proto);
907                if (not RT::Crypt->LoadImplementation($proto)) {
908                    $RT::Logger->error("You enabled $proto, but we couldn't load module RT::Crypt::$proto");
909                    $opt->{'Enable'} = 0;
910                } elsif (not RT::Crypt->LoadImplementation($proto)->Probe) {
911                    $opt->{'Enable'} = 0;
912                } elsif ($META{$proto}{'PostLoadCheck'}) {
913                    $META{$proto}{'PostLoadCheck'}->( $self, $self->Get( $proto ) );
914                }
915
916            }
917
918            my $opt = $self->Get('Crypt');
919            my @enabled = RT::Crypt->EnabledProtocols;
920            my %enabled;
921            $enabled{$_} = 1 for @enabled;
922            $opt->{'Enable'} = scalar @enabled;
923            $opt->{'Incoming'} = [ $opt->{'Incoming'} ]
924                if $opt->{'Incoming'} and not ref $opt->{'Incoming'};
925            if ( $opt->{'Incoming'} && @{ $opt->{'Incoming'} } ) {
926                $RT::Logger->warning("$_ explicitly set as incoming Crypt plugin, but not marked Enabled; removing")
927                    for grep {not $enabled{$_}} @{$opt->{'Incoming'}};
928                $opt->{'Incoming'} = [ grep {$enabled{$_}} @{$opt->{'Incoming'}} ];
929            } else {
930                $opt->{'Incoming'} = \@enabled;
931            }
932            if ( $opt->{'Outgoing'} ) {
933                if (ref($opt->{'Outgoing'}) eq 'HASH') {
934                    # Check each entry in the hash
935                    foreach my $q (keys(%{$opt->{'Outgoing'}})) {
936                        if (not $enabled{$opt->{'Outgoing'}->{$q}}) {
937                            if ($q ne '') {
938                                $RT::Logger->warning($opt->{'Outgoing'}->{$q}.
939                                                     " explicitly set as outgoing Crypt plugin for queue $q, but not marked Enabled; "
940                                                     . (@enabled ? "using $enabled[0]" : "removing"));
941                            } else {
942                                $RT::Logger->warning($opt->{'Outgoing'}->{$q}.
943                                                     " explicitly set as default outgoing Crypt plugin, but not marked Enabled; "
944                                                     . (@enabled ? "using $enabled[0]" : "removing"));
945                            }
946                            $opt->{'Outgoing'}->{$q} = $enabled[0];
947                        }
948                    }
949                    # If there's no entry for the default queue, set one
950                    if (!$opt->{'Outgoing'}->{''} && scalar(@enabled)) {
951                        $RT::Logger->warning("No default outgoing Crypt plugin set; using $enabled[0]");
952                        $opt->{'Outgoing'}->{''} = $enabled[0];
953                    }
954                } else {
955                    if (not $enabled{$opt->{'Outgoing'}}) {
956                        $RT::Logger->warning($opt->{'Outgoing'}.
957                                             " explicitly set as outgoing Crypt plugin, but not marked Enabled; "
958                                             . (@enabled ? "using $enabled[0]" : "removing"));
959                    }
960                    $opt->{'Outgoing'} = $enabled[0] unless $enabled{$opt->{'Outgoing'}};
961                }
962            } else {
963                $opt->{'Outgoing'} = $enabled[0];
964            }
965        },
966    },
967    SMIME        => {
968        Type => 'HASH',
969        Immutable => 1,
970        Invisible => 1,
971        Obfuscate => sub {
972            my ( $config, $value, $user ) = @_;
973            $value->{Passphrase} = $user->loc('Password not printed');
974            return $value;
975        },
976        PostLoadCheck => sub {
977            my $self = shift;
978            my $opt = $self->Get('SMIME');
979            return unless $opt->{'Enable'};
980
981            if (exists $opt->{Keyring}) {
982                unless ( File::Spec->file_name_is_absolute( $opt->{Keyring} ) ) {
983                    $opt->{Keyring} = File::Spec->catfile( $RT::BasePath, $opt->{Keyring} );
984                }
985                unless (-d $opt->{Keyring} and -r _) {
986                    $RT::Logger->info(
987                        "RT's SMIME libraries couldn't successfully read your".
988                        " configured SMIME keyring directory (".$opt->{Keyring}
989                        .").");
990                    delete $opt->{Keyring};
991                }
992            }
993
994            if (defined $opt->{CAPath}) {
995                if (-d $opt->{CAPath} and -r _) {
996                    # directory, all set
997                } elsif (-f $opt->{CAPath} and -r _) {
998                    # file, all set
999                } else {
1000                    $RT::Logger->warn(
1001                        "RT's SMIME libraries could not read your configured CAPath (".$opt->{CAPath}.")"
1002                    );
1003                    delete $opt->{CAPath};
1004                }
1005            }
1006
1007            if ($opt->{CheckCRL} && ! RT::Crypt::SMIME->SupportsCRLfile) {
1008                $opt->{CheckCRL} = 0;
1009                $RT::Logger->warn(
1010                    "Your version of OpenSSL does not support the -CRLfile option; disabling \$SMIME{CheckCRL}"
1011                );
1012            }
1013        },
1014    },
1015    GnuPG        => {
1016        Type => 'HASH',
1017        Immutable => 1,
1018        Invisible => 1,
1019        Obfuscate => sub {
1020            my ( $config, $value, $user ) = @_;
1021            $value->{Passphrase} = $user->loc('Password not printed');
1022            return $value;
1023        },
1024        PostLoadCheck => sub {
1025            my $self = shift;
1026            my $gpg = $self->Get('GnuPG');
1027            return unless $gpg->{'Enable'};
1028
1029            my $gpgopts = $self->Get('GnuPGOptions');
1030            unless ( File::Spec->file_name_is_absolute( $gpgopts->{homedir} ) ) {
1031                $gpgopts->{homedir} = File::Spec->catfile( $RT::BasePath, $gpgopts->{homedir} );
1032            }
1033            unless (-d $gpgopts->{homedir}  && -r _ ) { # no homedir, no gpg
1034                $RT::Logger->info(
1035                    "RT's GnuPG libraries couldn't successfully read your".
1036                    " configured GnuPG home directory (".$gpgopts->{homedir}
1037                    ."). GnuPG support has been disabled");
1038                $gpg->{'Enable'} = 0;
1039                return;
1040            }
1041
1042            if ( grep exists $gpg->{$_}, qw(RejectOnMissingPrivateKey RejectOnBadData AllowEncryptDataInDB) ) {
1043                $RT::Logger->warning(
1044                    "The RejectOnMissingPrivateKey, RejectOnBadData and AllowEncryptDataInDB"
1045                    ." GnuPG options are now properties of the generic Crypt configuration. You"
1046                    ." should set them there instead."
1047                );
1048                delete $gpg->{$_} for qw(RejectOnMissingPrivateKey RejectOnBadData AllowEncryptDataInDB);
1049            }
1050        }
1051    },
1052    GnuPGOptions => {
1053        Type      => 'HASH',
1054        Immutable => 1,
1055        Invisible => 1,
1056        Obfuscate => sub {
1057            my ( $config, $value, $user ) = @_;
1058            $value->{passphrase} = $user->loc('Password not printed');
1059            return $value;
1060        },
1061    },
1062    ReferrerWhitelist => { Type => 'ARRAY' },
1063    EmailDashboardLanguageOrder  => { Type => 'ARRAY' },
1064    CustomFieldValuesCanonicalizers => { Type => 'ARRAY' },
1065    WebPath => {
1066        Immutable     => 1,
1067        Widget        => '/Widgets/Form/String',
1068        PostLoadCheck => sub {
1069            my $self  = shift;
1070            my $value = shift;
1071
1072            # "In most cases, you should leave $WebPath set to '' (an empty value)."
1073            return unless $value;
1074
1075            # try to catch someone who assumes that you shouldn't leave this empty
1076            if ($value eq '/') {
1077                $RT::Logger->error("For the WebPath config option, use the empty string instead of /");
1078                return;
1079            }
1080
1081            # $WebPath requires a leading / but no trailing /, or it can be blank.
1082            return if $value =~ m{^/.+[^/]$};
1083
1084            if ($value =~ m{/$}) {
1085                $RT::Logger->error("The WebPath config option requires no trailing slash");
1086            }
1087
1088            if ($value !~ m{^/}) {
1089                $RT::Logger->error("The WebPath config option requires a leading slash");
1090            }
1091        },
1092    },
1093    WebDomain => {
1094        Immutable     => 1,
1095        Widget        => '/Widgets/Form/String',
1096        PostLoadCheck => sub {
1097            my $self  = shift;
1098            my $value = shift;
1099
1100            if (!$value) {
1101                $RT::Logger->error("You must set the WebDomain config option");
1102                return;
1103            }
1104
1105            if ($value =~ m{^(\w+://)}) {
1106                $RT::Logger->error("The WebDomain config option must not contain a scheme ($1)");
1107                return;
1108            }
1109
1110            if ($value =~ m{(/.*)}) {
1111                $RT::Logger->error("The WebDomain config option must not contain a path ($1)");
1112                return;
1113            }
1114
1115            if ($value =~ m{:(\d*)}) {
1116                $RT::Logger->error("The WebDomain config option must not contain a port ($1)");
1117                return;
1118            }
1119        },
1120    },
1121    WebPort => {
1122        Immutable     => 1,
1123        Widget        => '/Widgets/Form/Integer',
1124        PostLoadCheck => sub {
1125            my $self  = shift;
1126            my $value = shift;
1127
1128            if (!$value) {
1129                $RT::Logger->error("You must set the WebPort config option");
1130                return;
1131            }
1132
1133            if ($value !~ m{^\d+$}) {
1134                $RT::Logger->error("The WebPort config option must be an integer");
1135            }
1136        },
1137    },
1138    WebBaseURL => {
1139        Immutable     => 1,
1140        Widget        => '/Widgets/Form/String',
1141        PostLoadCheck => sub {
1142            my $self  = shift;
1143            my $value = shift;
1144
1145            if (!$value) {
1146                $RT::Logger->error("You must set the WebBaseURL config option");
1147                return;
1148            }
1149
1150            if ($value !~ m{^https?://}i) {
1151                $RT::Logger->error("The WebBaseURL config option must contain a scheme (http or https)");
1152            }
1153
1154            if ($value =~ m{/$}) {
1155                $RT::Logger->error("The WebBaseURL config option requires no trailing slash");
1156            }
1157
1158            if ($value =~ m{^https?://.+?(/[^/].*)}i) {
1159                $RT::Logger->error("The WebBaseURL config option must not contain a path ($1)");
1160            }
1161        },
1162    },
1163    WebURL => {
1164        Immutable     => 1,
1165        Widget => '/Widgets/Form/String',
1166        PostLoadCheck => sub {
1167            my $self  = shift;
1168            my $value = shift;
1169
1170            if (!$value) {
1171                $RT::Logger->error("You must set the WebURL config option");
1172                return;
1173            }
1174
1175            if ($value !~ m{^https?://}i) {
1176                $RT::Logger->error("The WebURL config option must contain a scheme (http or https)");
1177            }
1178
1179            if ($value !~ m{/$}) {
1180                $RT::Logger->error("The WebURL config option requires a trailing slash");
1181            }
1182        },
1183    },
1184    EmailInputEncodings => {
1185        Type => 'ARRAY',
1186        PostLoadCheck => sub {
1187            my $self  = shift;
1188            my $value = $self->Get('EmailInputEncodings');
1189            return unless $value && @$value;
1190
1191            my %seen;
1192            foreach my $encoding ( grep defined && length, splice @$value ) {
1193                next if $seen{ $encoding };
1194                if ( $encoding eq '*' ) {
1195                    unshift @$value, '*';
1196                    next;
1197                }
1198
1199                my $canonic = Encode::resolve_alias( $encoding );
1200                unless ( $canonic ) {
1201                    $RT::Logger->warning("Unknown encoding '$encoding' in \@EmailInputEncodings option");
1202                }
1203                elsif ( $seen{ $canonic }++ ) {
1204                    next;
1205                }
1206                else {
1207                    push @$value, $canonic;
1208                }
1209            }
1210        },
1211    },
1212    CustomFieldGroupings => {
1213        Type            => 'HASH',
1214        PostLoadCheck   => sub {
1215            my $config = shift;
1216            # use scalar context intentionally to avoid not a hash error
1217            my $groups = $config->Get('CustomFieldGroupings') || {};
1218
1219            unless (ref($groups) eq 'HASH') {
1220                RT->Logger->error("Config option \%CustomFieldGroupings is a @{[ref $groups]} not a HASH; ignoring");
1221                $groups = {};
1222            }
1223
1224            for my $class (keys %$groups) {
1225                my @h;
1226                if (ref($groups->{$class}) eq 'HASH') {
1227                    push @h, $_, $groups->{$class}->{$_}
1228                        for sort {lc($a) cmp lc($b)} keys %{ $groups->{$class} };
1229                } elsif (ref($groups->{$class}) eq 'ARRAY') {
1230                    @h = @{ $groups->{$class} };
1231                } else {
1232                    RT->Logger->error("Config option \%CustomFieldGroupings{$class} is not a HASH or ARRAY; ignoring");
1233                    delete $groups->{$class};
1234                    next;
1235                }
1236
1237                $groups->{$class} = [];
1238                while (@h) {
1239                    my $group = shift @h;
1240                    my $ref   = shift @h;
1241                    if (ref($ref) eq 'ARRAY') {
1242                        push @{$groups->{$class}}, $group => $ref;
1243                    } else {
1244                        RT->Logger->error("Config option \%CustomFieldGroupings{$class}{$group} is not an ARRAY; ignoring");
1245                    }
1246                }
1247            }
1248            $config->Set( CustomFieldGroupings => %$groups );
1249        },
1250    },
1251    CustomDateRanges => {
1252        Type            => 'HASH',
1253        Widget          => '/Widgets/Form/CustomDateRanges',
1254        PostLoadCheck   => sub {
1255            my $config = shift;
1256            # use scalar context intentionally to avoid not a hash error
1257            my $ranges = $config->Get('CustomDateRanges') || {};
1258
1259            unless (ref($ranges) eq 'HASH') {
1260                RT->Logger->error("Config option \%CustomDateRanges is a @{[ref $ranges]} not a HASH");
1261                return;
1262            }
1263
1264            for my $class (keys %$ranges) {
1265                if (ref($ranges->{$class}) eq 'HASH') {
1266                    for my $name (keys %{ $ranges->{$class} }) {
1267                        my $spec = $ranges->{$class}{$name};
1268                        if (!ref($spec) || ref($spec) eq 'HASH') {
1269                            # this will produce error messages if parsing fails
1270                            $class->require;
1271                            $class->_ParseCustomDateRangeSpec($name, $spec);
1272                        }
1273                        else {
1274                            RT->Logger->error("Config option \%CustomDateRanges{$class}{$name} is not a string or HASH");
1275                        }
1276                    }
1277                } else {
1278                    RT->Logger->error("Config option \%CustomDateRanges{$class} is not a HASH");
1279                }
1280            }
1281
1282            my %system_config = %$ranges;
1283            if ( my $db_config = $config->Get('CustomDateRangesUI') ) {
1284                for my $type ( keys %$db_config ) {
1285                    for my $name ( keys %{ $db_config->{$type} || {} } ) {
1286                        if ( $system_config{$type}{$name} ) {
1287                            RT->Logger->warning("$type custom date range $name is defined by config file and db");
1288                        }
1289                        else {
1290                            $system_config{$name} = $db_config->{$type}{$name};
1291                        }
1292                    }
1293                }
1294            }
1295
1296            for my $type ( keys %system_config ) {
1297                my $attributes = RT::Attributes->new( RT->SystemUser );
1298                $attributes->Limit( FIELD => 'Name',       VALUE => 'Pref-CustomDateRanges' );
1299                $attributes->Limit( FIELD => 'ObjectType', VALUE => 'RT::User' );
1300                $attributes->OrderBy( FIELD => 'id' );
1301
1302                while ( my $attribute = $attributes->Next ) {
1303                    if ( my $content = $attribute->Content ) {
1304                        for my $name ( keys %{ $content->{$type} || {} } ) {
1305                            if ( $system_config{$type}{$name} ) {
1306                                RT->Logger->warning( "$type custom date range $name is defined by system and user #"
1307                                        . $attribute->ObjectId );
1308                            }
1309                        }
1310                    }
1311                }
1312            }
1313        },
1314    },
1315    CustomDateRangesUI => {
1316        Type            => 'HASH',
1317        Widget          => '/Widgets/Form/CustomDateRanges',
1318    },
1319    ExternalStorage => {
1320        Type            => 'HASH',
1321        PostLoadCheck   => sub {
1322            my $self = shift;
1323            my %hash = $self->Get('ExternalStorage');
1324            return unless keys %hash;
1325
1326            require RT::ExternalStorage;
1327
1328            my $backend = RT::ExternalStorage::Backend->new(%hash);
1329            RT->System->ExternalStorage($backend);
1330        },
1331    },
1332    ChartColors => {
1333        Type    => 'ARRAY',
1334    },
1335    LogoImageHeight => {
1336        Deprecated => {
1337            LogLevel => "info",
1338            Message => "The LogoImageHeight configuration option did not affect display, and has been removed; please remove it from your RT_SiteConfig.pm",
1339        },
1340    },
1341    LogoImageWidth => {
1342        Deprecated => {
1343            LogLevel => "info",
1344            Message => "The LogoImageWidth configuration option did not affect display, and has been removed; please remove it from your RT_SiteConfig.pm",
1345        },
1346    },
1347
1348    ExternalAuth => {
1349        Immutable => 1,
1350        Widget    => '/Widgets/Form/Boolean',
1351    },
1352
1353    DisablePasswordForAuthToken => {
1354        Widget => '/Widgets/Form/Boolean',
1355    },
1356
1357    ExternalSettings => {
1358        Immutable     => 1,
1359        Obfuscate => sub {
1360            # Ensure passwords are obfuscated on the System Configuration page
1361            my ($config, $sources, $user) = @_;
1362            my $msg = $user->loc('Password not printed');
1363
1364            for my $source (values %$sources) {
1365                $source->{pass} = $msg;
1366            }
1367            return $sources;
1368        },
1369        PostLoadCheck => sub {
1370            my $self = shift;
1371            my $settings = shift || {};
1372
1373            $self->EnableExternalAuth() if keys %$settings > 0;
1374
1375            my $remove = sub {
1376                my ($service) = @_;
1377                delete $settings->{$service};
1378
1379                $self->Set( 'ExternalAuthPriority',
1380                        [ grep { $_ ne $service } @{ $self->Get('ExternalAuthPriority') || [] } ] );
1381
1382                $self->Set( 'ExternalInfoPriority',
1383                        [ grep { $_ ne $service } @{ $self->Get('ExternalInfoPriority') || [] } ] );
1384            };
1385
1386            for my $service (keys %$settings) {
1387                my %conf = %{ $settings->{$service} };
1388
1389                if ($conf{type} !~ /^(ldap|db|cookie)$/) {
1390                    $RT::Logger->error(
1391                        "Service '$service' in ExternalInfoPriority is not ldap, db, or cookie; removing."
1392                    );
1393                    $remove->($service);
1394                    next;
1395                }
1396
1397                next unless $conf{type} eq 'db';
1398
1399                # Ensure people don't misconfigure DBI auth to point to RT's
1400                # Users table; only check server/hostname/table, as
1401                # user/pass might be different (root, for instance)
1402                no warnings 'uninitialized';
1403                next unless lc $conf{server} eq lc RT->Config->Get('DatabaseHost') and
1404                        lc $conf{database} eq lc RT->Config->Get('DatabaseName') and
1405                        lc $conf{table} eq 'users';
1406
1407                $RT::Logger->error(
1408                    "RT::Authen::ExternalAuth should _not_ be configured with a database auth service ".
1409                    "that points back to RT's internal Users table.  Removing the service '$service'! ".
1410                    "Please remove it from your config file."
1411                );
1412
1413                $remove->($service);
1414            }
1415            $self->Set( 'ExternalSettings', $settings );
1416        },
1417    },
1418
1419    ExternalAuthPriority => {
1420        Immutable     => 1,
1421        PostLoadCheck => sub {
1422            my $self = shift;
1423            my @values = @{ shift || [] };
1424
1425            return unless @values or $self->Get('ExternalSettings');
1426
1427            if (not @values) {
1428                $RT::Logger->debug("ExternalAuthPriority not defined. Attempting to create based on ExternalSettings");
1429                $self->Set( 'ExternalAuthPriority', \@values );
1430                return;
1431            }
1432            my %settings;
1433            if ( $self->Get('ExternalSettings') ){
1434                %settings = %{ $self->Get('ExternalSettings') };
1435            }
1436            else{
1437                $RT::Logger->error("ExternalSettings not defined. ExternalAuth requires the ExternalSettings configuration option to operate properly");
1438                return;
1439            }
1440            for my $key (grep {not $settings{$_}} @values) {
1441                $RT::Logger->error("Removing '$key' from ExternalAuthPriority, as it is not defined in ExternalSettings");
1442            }
1443            @values = grep {$settings{$_}} @values;
1444            $self->Set( 'ExternalAuthPriority', \@values );
1445        },
1446    },
1447
1448    ExternalInfoPriority => {
1449        Immutable     => 1,
1450        PostLoadCheck => sub {
1451            my $self = shift;
1452            my @values = @{ shift || [] };
1453
1454            return unless @values or $self->Get('ExternalSettings');
1455
1456            if (not @values) {
1457                $RT::Logger->debug("ExternalInfoPriority not defined. User information (including user enabled/disabled) cannot be externally-sourced");
1458                $self->Set( 'ExternalInfoPriority', \@values );
1459                return;
1460            }
1461
1462            my %settings;
1463            if ( $self->Get('ExternalSettings') ){
1464                %settings = %{ $self->Get('ExternalSettings') };
1465            }
1466            else{
1467                $RT::Logger->error("ExternalSettings not defined. ExternalAuth requires the ExternalSettings configuration option to operate properly");
1468                return;
1469            }
1470            for my $key (grep {not $settings{$_}} @values) {
1471                $RT::Logger->error("Removing '$key' from ExternalInfoPriority, as it is not defined in ExternalSettings");
1472            }
1473            @values = grep {$settings{$_}} @values;
1474
1475            for my $key (grep {$settings{$_}{type} eq "cookie"} @values) {
1476                $RT::Logger->error("Removing '$key' from ExternalInfoPriority, as cookie authentication cannot be used as an information source");
1477            }
1478            @values = grep {$settings{$_}{type} ne "cookie"} @values;
1479
1480            $self->Set( 'ExternalInfoPriority', \@values );
1481        },
1482    },
1483    PriorityAsString => {
1484        Type          => 'HASH',
1485        PostLoadCheck => sub {
1486            my $self = shift;
1487            return unless $self->Get('EnablePriorityAsString');
1488            my $config = $self->Get('PriorityAsString');
1489
1490            my %map;
1491
1492            for my $name ( keys %$config ) {
1493                if ( my $value = $config->{$name} ) {
1494                    my @list;
1495                    if ( ref $value eq 'ARRAY' ) {
1496                        @list = @$value;
1497                    }
1498                    elsif ( ref $value eq 'HASH' ) {
1499                        @list = %$value;
1500                    }
1501                    else {
1502                        RT->Logger->error("Invalid value for $name in PriorityAsString");
1503                        undef $config->{$name};
1504                    }
1505
1506                    while ( my $label = shift @list ) {
1507                        my $value = shift @list;
1508                        $map{$label} //= $value;
1509
1510                        if ( $map{$label} != $value ) {
1511                            RT->Logger->debug("Priority $label is inconsistent: $map{$label} VS $value");
1512                        }
1513                    }
1514
1515                }
1516            }
1517
1518            unless ( keys %map ) {
1519                RT->Logger->debug("No valid PriorityAsString options");
1520                $self->Set( 'EnablePriorityAsString', 0 );
1521            }
1522        },
1523    },
1524    ServiceBusinessHours => {
1525        Type => 'HASH',
1526        PostLoadCheck   => sub {
1527            my $self = shift;
1528            my $config = $self->Get('ServiceBusinessHours');
1529            for my $name (keys %$config) {
1530                if ($config->{$name}->{7}) {
1531                    RT->Logger->error("Config option \%ServiceBusinessHours '$name' erroneously specifies '$config->{$name}->{7}->{Name}' as day 7; Sunday should be specified as day 0.");
1532                }
1533            }
1534        },
1535    },
1536    ServiceAgreements => {
1537        Type => 'HASH',
1538    },
1539    AssetHideSimpleSearch => {
1540        Widget => '/Widgets/Form/Boolean',
1541    },
1542    AssetMultipleOwner => {
1543        Widget => '/Widgets/Form/Boolean',
1544    },
1545    AssetShowSearchResultCount => {
1546        Widget => '/Widgets/Form/Boolean',
1547    },
1548    AllowUserAutocompleteForUnprivileged => {
1549        Widget => '/Widgets/Form/Boolean',
1550    },
1551    AlwaysDownloadAttachments => {
1552        Widget => '/Widgets/Form/Boolean',
1553    },
1554    AmbiguousDayInFuture => {
1555        Widget => '/Widgets/Form/Boolean',
1556    },
1557    AmbiguousDayInPast => {
1558        Widget => '/Widgets/Form/Boolean',
1559    },
1560    ApprovalRejectionNotes => {
1561        Widget => '/Widgets/Form/Boolean',
1562    },
1563    ArticleOnTicketCreate => {
1564        Widget => '/Widgets/Form/Boolean',
1565    },
1566    AutoCreateNonExternalUsers => {
1567        Widget => '/Widgets/Form/Boolean',
1568    },
1569    AutocompleteOwnersForSearch => {
1570        Widget => '/Widgets/Form/Boolean',
1571    },
1572    CanonicalizeRedirectURLs => {
1573        Widget => '/Widgets/Form/Boolean',
1574    },
1575    CanonicalizeURLsInFeeds => {
1576        Widget => '/Widgets/Form/Boolean',
1577    },
1578    ChartsTimezonesInDB => {
1579        Widget => '/Widgets/Form/Boolean',
1580    },
1581    CheckMoreMSMailHeaders => {
1582        Widget => '/Widgets/Form/Boolean',
1583    },
1584    DateDayBeforeMonth => {
1585        Widget => '/Widgets/Form/Boolean',
1586    },
1587    DisplayTotalTimeWorked => {
1588        Widget => '/Widgets/Form/Boolean',
1589    },
1590    DontSearchFileAttachments => {
1591        Widget => '/Widgets/Form/Boolean',
1592    },
1593    DropLongAttachments => {
1594        Widget => '/Widgets/Form/Boolean',
1595    },
1596    EditCustomFieldsSingleColumn => {
1597        Widget => '/Widgets/Form/Boolean',
1598    },
1599    EnableReminders => {
1600        Widget => '/Widgets/Form/Boolean',
1601    },
1602    EnablePriorityAsString => {
1603        Widget => '/Widgets/Form/Boolean',
1604    },
1605    ExternalStorageDirectLink => {
1606        Widget => '/Widgets/Form/Boolean',
1607    },
1608    ForceApprovalsView => {
1609        Widget => '/Widgets/Form/Boolean',
1610    },
1611    ForwardFromUser => {
1612        Widget => '/Widgets/Form/Boolean',
1613    },
1614    Framebusting => {
1615        Widget => '/Widgets/Form/Boolean',
1616    },
1617    HideArticleSearchOnReplyCreate => {
1618        Widget => '/Widgets/Form/Boolean',
1619    },
1620    HideResolveActionsWithDependencies => {
1621        Widget => '/Widgets/Form/Boolean',
1622    },
1623    HideTimeFieldsFromUnprivilegedUsers => {
1624        Widget => '/Widgets/Form/Boolean',
1625    },
1626    LoopsToRTOwner => {
1627        Widget => '/Widgets/Form/Boolean',
1628    },
1629    MessageBoxIncludeSignature => {
1630        Widget => '/Widgets/Form/Boolean',
1631    },
1632    MessageBoxIncludeSignatureOnComment => {
1633        Widget => '/Widgets/Form/Boolean',
1634    },
1635    OnlySearchActiveTicketsInSimpleSearch => {
1636        Widget => '/Widgets/Form/Boolean',
1637    },
1638    ParseNewMessageForTicketCcs => {
1639        Widget => '/Widgets/Form/Boolean',
1640    },
1641    PreferDateTimeFormatNatural => {
1642        Widget => '/Widgets/Form/Boolean',
1643    },
1644    PreviewScripMessages => {
1645        Widget => '/Widgets/Form/Boolean',
1646    },
1647    RecordOutgoingEmail => {
1648        Widget => '/Widgets/Form/Boolean',
1649    },
1650    RestrictLoginReferrer => {
1651        Widget => '/Widgets/Form/Boolean',
1652    },
1653    RestrictReferrer => {
1654        Widget => '/Widgets/Form/Boolean',
1655    },
1656    SearchResultsAutoRedirect => {
1657        Widget => '/Widgets/Form/Boolean',
1658    },
1659    SelfServiceUseDashboard => {
1660        Widget => '/Widgets/Form/Boolean',
1661    },
1662    ShowBccHeader => {
1663        Widget => '/Widgets/Form/Boolean',
1664    },
1665    ShowEditSystemConfig => {
1666        Immutable => 1,
1667        Widget    => '/Widgets/Form/Boolean',
1668    },
1669    ShowEditLifecycleConfig => {
1670        Immutable => 1,
1671        Widget    => '/Widgets/Form/Boolean',
1672    },
1673    ShowMoreAboutPrivilegedUsers => {
1674        Widget => '/Widgets/Form/Boolean',
1675    },
1676    ShowRTPortal => {
1677        Widget => '/Widgets/Form/Boolean',
1678    },
1679    ShowRemoteImages => {
1680        Widget => '/Widgets/Form/Boolean',
1681    },
1682    ShowTransactionImages => {
1683        Widget => '/Widgets/Form/Boolean',
1684    },
1685    StoreLoops => {
1686        Widget => '/Widgets/Form/Boolean',
1687    },
1688    StrictLinkACL => {
1689        Widget => '/Widgets/Form/Boolean',
1690    },
1691    SuppressInlineTextFiles => {
1692        Widget => '/Widgets/Form/Boolean',
1693    },
1694    TreatAttachedEmailAsFiles => {
1695        Widget => '/Widgets/Form/Boolean',
1696    },
1697    TruncateLongAttachments => {
1698        Widget => '/Widgets/Form/Boolean',
1699    },
1700    TrustHTMLAttachments => {
1701        Widget => '/Widgets/Form/Boolean',
1702    },
1703    UseFriendlyFromLine => {
1704        Widget => '/Widgets/Form/Boolean',
1705    },
1706    UseFriendlyToLine => {
1707        Widget => '/Widgets/Form/Boolean',
1708    },
1709    UseOriginatorHeader => {
1710        Widget => '/Widgets/Form/Boolean',
1711    },
1712    UseSQLForACLChecks => {
1713        Widget => '/Widgets/Form/Boolean',
1714    },
1715    UseTransactionBatch => {
1716        Widget => '/Widgets/Form/Boolean',
1717    },
1718    ValidateUserEmailAddresses => {
1719        Widget => '/Widgets/Form/Boolean',
1720    },
1721    WebFallbackToRTLogin => {
1722        Widget => '/Widgets/Form/Boolean',
1723    },
1724    WebFlushDbCacheEveryRequest => {
1725        Widget => '/Widgets/Form/Boolean',
1726    },
1727    WebHttpOnlyCookies => {
1728        Widget => '/Widgets/Form/Boolean',
1729    },
1730    WebRemoteUserAuth => {
1731        Widget => '/Widgets/Form/Boolean',
1732    },
1733    WebRemoteUserAutocreate => {
1734        Widget => '/Widgets/Form/Boolean',
1735    },
1736    WebRemoteUserContinuous => {
1737        Widget => '/Widgets/Form/Boolean',
1738    },
1739    WebRemoteUserGecos => {
1740        Widget => '/Widgets/Form/Boolean',
1741    },
1742    WebSecureCookies => {
1743        Widget => '/Widgets/Form/Boolean',
1744    },
1745    WikiImplicitLinks => {
1746        Widget => '/Widgets/Form/Boolean',
1747    },
1748    HideOneTimeSuggestions => {
1749        Widget => '/Widgets/Form/Boolean',
1750    },
1751    LinkArticlesOnInclude => {
1752        Widget => '/Widgets/Form/Boolean',
1753    },
1754    SelfServiceCorrespondenceOnly => {
1755        Widget => '/Widgets/Form/Boolean',
1756    },
1757    SelfServiceDownloadUserData => {
1758        Widget => '/Widgets/Form/Boolean',
1759    },
1760    SelfServiceShowGroupTickets => {
1761        Widget => '/Widgets/Form/Boolean',
1762    },
1763    SelfServiceShowArticleSearch => {
1764        Widget => '/Widgets/Form/Boolean',
1765    },
1766    ShowSearchResultCount => {
1767        Widget => '/Widgets/Form/Boolean',
1768    },
1769    AllowGroupAutocompleteForUnprivileged => {
1770        Widget => '/Widgets/Form/Boolean',
1771    },
1772
1773    AttachmentListCount => {
1774        Widget => '/Widgets/Form/Integer',
1775    },
1776    AutoLogoff => {
1777        Widget => '/Widgets/Form/Integer',
1778    },
1779    BcryptCost => {
1780        Widget => '/Widgets/Form/Integer',
1781    },
1782    DefaultSummaryRows => {
1783        Widget => '/Widgets/Form/Integer',
1784    },
1785    DropdownMenuLimit => {
1786        Widget => '/Widgets/Form/Integer',
1787    },
1788    ExternalStorageCutoffSize => {
1789        Widget => '/Widgets/Form/Integer',
1790    },
1791    LogoutRefresh => {
1792        Widget => '/Widgets/Form/Integer',
1793    },
1794    MaxAttachmentSize => {
1795        Widget => '/Widgets/Form/Integer',
1796    },
1797    MaxFulltextAttachmentSize => {
1798        Widget => '/Widgets/Form/Integer',
1799    },
1800    MinimumPasswordLength => {
1801        Widget => '/Widgets/Form/Integer',
1802    },
1803    MoreAboutRequestorGroupsLimit => {
1804        Widget => '/Widgets/Form/Integer',
1805    },
1806    TicketsItemMapSize => {
1807        Widget => '/Widgets/Form/Integer',
1808    },
1809
1810    AssetDefaultSearchResultOrderBy => {
1811        Widget => '/Widgets/Form/String',
1812    },
1813    CanonicalizeEmailAddressMatch => {
1814        Widget => '/Widgets/Form/String',
1815    },
1816    CanonicalizeEmailAddressReplace => {
1817        Widget => '/Widgets/Form/String',
1818    },
1819    CommentAddress => {
1820        Widget => '/Widgets/Form/String',
1821    },
1822    CorrespondAddress => {
1823        Widget => '/Widgets/Form/String',
1824    },
1825    DashboardAddress => {
1826        Widget => '/Widgets/Form/String',
1827    },
1828    DashboardSubject => {
1829        Widget => '/Widgets/Form/String',
1830    },
1831    DefaultErrorMailPrecedence => {
1832        Widget => '/Widgets/Form/String',
1833    },
1834    DefaultMailPrecedence => {
1835        Widget => '/Widgets/Form/String',
1836    },
1837    DefaultSearchResultOrderBy => {
1838        Widget => '/Widgets/Form/String',
1839    },
1840    EmailOutputEncoding => {
1841        Widget => '/Widgets/Form/String',
1842    },
1843    FriendlyFromLineFormat => {
1844        Widget => '/Widgets/Form/String',
1845    },
1846    FriendlyToLineFormat => {
1847        Widget => '/Widgets/Form/String',
1848    },
1849    LDAPHost => {
1850        Widget => '/Widgets/Form/String',
1851    },
1852    LDAPUser => {
1853        Widget => '/Widgets/Form/String',
1854    },
1855    LDAPPassword => {
1856        Widget => '/Widgets/Form/String',
1857        Obfuscate => sub {
1858            my ($config, $sources, $user) = @_;
1859            return $user->loc('Password not printed');
1860        },
1861    },
1862    LDAPBase => {
1863        Widget => '/Widgets/Form/String',
1864    },
1865    LDAPGroupBase => {
1866        Widget => '/Widgets/Form/String',
1867    },
1868    LogDir => {
1869        Immutable => 1,
1870        Widget => '/Widgets/Form/String',
1871    },
1872    LogToFileNamed => {
1873        Immutable => 1,
1874        Widget => '/Widgets/Form/String',
1875    },
1876    LogoAltText => {
1877        Widget => '/Widgets/Form/String',
1878    },
1879    LogoLinkURL => {
1880        Widget => '/Widgets/Form/String',
1881    },
1882    LogoURL => {
1883        Widget => '/Widgets/Form/String',
1884    },
1885    LogoutURL => {
1886        Widget => '/Widgets/Form/String',
1887    },
1888    OwnerEmail => {
1889        Widget => '/Widgets/Form/String',
1890    },
1891    QuoteWrapWidth => {
1892        Widget => '/Widgets/Form/Integer',
1893    },
1894    RedistributeAutoGeneratedMessages => {
1895        Widget => '/Widgets/Form/String',
1896    },
1897    RTSupportEmail => {
1898        Widget => '/Widgets/Form/String',
1899    },
1900    SelfServiceRequestUpdateQueue => {
1901        Widget => '/Widgets/Form/String',
1902    },
1903    SendmailArguments => {
1904        Widget => '/Widgets/Form/String',
1905    },
1906    SendmailBounceArguments => {
1907        Widget => '/Widgets/Form/String',
1908    },
1909    SendmailPath => {
1910        Widget => '/Widgets/Form/String',
1911    },
1912    SetOutgoingMailFrom => {
1913        Widget => '/Widgets/Form/String',
1914    },
1915    Timezone => {
1916        Widget => '/Widgets/Form/String',
1917    },
1918    VERPPrefix => {
1919        Widget => '/Widgets/Form/String',
1920        WidgetArguments => { Hints  => 'rt-', },
1921    },
1922    VERPDomain => {
1923        Widget => '/Widgets/Form/String',
1924        WidgetArguments => {
1925            Callback => sub {  return { Hints => RT->Config->Get( 'Organization') } },
1926        },
1927    },
1928    WebImagesURL => {
1929        Widget => '/Widgets/Form/String',
1930    },
1931
1932    AssetDefaultSearchResultOrder => {
1933        Widget => '/Widgets/Form/Select',
1934        WidgetArguments => { Values => [qw(ASC DESC)] },
1935    },
1936    LogToSyslog => {
1937        Immutable => 1,
1938        Widget => '/Widgets/Form/Select',
1939        WidgetArguments => { Values => [qw(debug info notice warning error critical alert emergency)] },
1940    },
1941    LogToSTDERR => {
1942        Immutable => 1,
1943        Widget => '/Widgets/Form/Select',
1944        WidgetArguments => { Values => [qw(debug info notice warning error critical alert emergency)] },
1945    },
1946    LogToFile => {
1947        Immutable => 1,
1948        Widget => '/Widgets/Form/Select',
1949        WidgetArguments => { Values => [qw(debug info notice warning error critical alert emergency)] },
1950    },
1951    LogStackTraces => {
1952        Immutable => 1,
1953        Widget => '/Widgets/Form/Select',
1954        WidgetArguments => { Values => [qw(debug info notice warning error critical alert emergency)] },
1955    },
1956    StatementLog => {
1957        Widget => '/Widgets/Form/Select',
1958        WidgetArguments => { Values => ['', qw(debug info notice warning error critical alert emergency)] },
1959    },
1960
1961    DefaultCatalog => {
1962        Widget          => '/Widgets/Form/Select',
1963        WidgetArguments => {
1964            Description => 'Default catalog',    #loc
1965            Callback    => sub {
1966                my $ret = { Values => [], ValuesLabel => {} };
1967                my $c = RT::Catalogs->new( $HTML::Mason::Commands::session{'CurrentUser'} );
1968                $c->UnLimit;
1969                while ( my $catalog = $c->Next ) {
1970                    next unless $catalog->CurrentUserHasRight("CreateAsset");
1971                    push @{ $ret->{Values} }, $catalog->Id;
1972                    $ret->{ValuesLabel}{ $catalog->Id } = $catalog->Name;
1973                }
1974                return $ret;
1975            },
1976        }
1977    },
1978    DefaultSearchResultOrder => {
1979        Widget => '/Widgets/Form/Select',
1980        WidgetArguments => { Values => [qw(ASC DESC)] },
1981    },
1982    SelfServiceUserPrefs => {
1983        Widget          => '/Widgets/Form/Select',
1984        WidgetArguments => {
1985            Values      => [qw(edit-prefs view-info edit-prefs-view-info full-edit)],
1986            ValuesLabel => {
1987                'edit-prefs'           => 'Edit Locale and change password',                           # loc
1988                'view-info'            => 'View all the info',                                         # loc
1989                'edit-prefs-view-info' => 'View all the info, and edit Locale and change password',    # loc
1990                'full-edit'            => 'View and update all the info',                              # loc
1991            },
1992        },
1993    },
1994    AssetDefaultSearchResultFormat => {
1995        Widget => '/Widgets/Form/MultilineString',
1996    },
1997    AssetSimpleSearchFormat => {
1998        Widget => '/Widgets/Form/MultilineString',
1999    },
2000    AssetSummaryFormat => {
2001        Widget => '/Widgets/Form/MultilineString',
2002    },
2003    AssetSummaryRelatedTicketsFormat => {
2004        Widget => '/Widgets/Form/MultilineString',
2005    },
2006    DefaultSearchResultFormat => {
2007        Widget => '/Widgets/Form/MultilineString',
2008    },
2009    DefaultSelfServiceSearchResultFormat => {
2010        Widget => '/Widgets/Form/MultilineString',
2011    },
2012    GroupSearchResultFormat => {
2013        Widget => '/Widgets/Form/MultilineString',
2014    },
2015    GroupSummaryExtraInfo => {
2016        Widget => '/Widgets/Form/MultilineString',
2017    },
2018    GroupSummaryTicketListFormat => {
2019        Widget => '/Widgets/Form/MultilineString',
2020    },
2021    LDAPFilter => {
2022        Widget => '/Widgets/Form/MultilineString',
2023    },
2024    LDAPGroupFilter => {
2025        Widget => '/Widgets/Form/MultilineString',
2026    },
2027    MoreAboutRequestorExtraInfo => {
2028        Widget => '/Widgets/Form/MultilineString',
2029    },
2030    MoreAboutRequestorTicketListFormat => {
2031        Widget => '/Widgets/Form/MultilineString',
2032    },
2033    UserDataResultFormat => {
2034        Widget => '/Widgets/Form/MultilineString',
2035    },
2036    UserSearchResultFormat => {
2037        Widget => '/Widgets/Form/MultilineString',
2038    },
2039    UserSummaryExtraInfo => {
2040        Widget => '/Widgets/Form/MultilineString',
2041    },
2042    UserSummaryTicketListFormat => {
2043        Widget => '/Widgets/Form/MultilineString',
2044    },
2045    UserTicketDataResultFormat => {
2046        Widget => '/Widgets/Form/MultilineString',
2047    },
2048    UserTransactionDataResultFormat => {
2049        Widget => '/Widgets/Form/MultilineString',
2050    },
2051    LogToSyslogConf => {
2052        Immutable     => 1,
2053    },
2054    ShowMobileSite => {
2055        Widget => '/Widgets/Form/Boolean',
2056    },
2057    StaticRoots => {
2058        Type      => 'ARRAY',
2059        Immutable => 1,
2060    },
2061    EmailSubjectTagRegex => {
2062        Immutable => 1,
2063    },
2064    ExtractSubjectTagMatch => {
2065        Immutable => 1,
2066    },
2067    ExtractSubjectTagNoMatch => {
2068        Immutable => 1,
2069    },
2070    WebNoAuthRegex => {
2071        Immutable => 1,
2072    },
2073    SelfServiceRegex => {
2074        Immutable => 1,
2075    },
2076);
2077my %OPTIONS = ();
2078my @LOADED_CONFIGS = ();
2079
2080=head1 METHODS
2081
2082=head2 new
2083
2084Object constructor returns new object. Takes no arguments.
2085
2086=cut
2087
2088sub new {
2089    my $proto = shift;
2090    my $class = ref($proto) ? ref($proto) : $proto;
2091    my $self  = bless {}, $class;
2092    $self->_Init(@_);
2093    return $self;
2094}
2095
2096sub _Init {
2097    return;
2098}
2099
2100=head2 LoadConfigs
2101
2102Load all configs. First of all load RT's config then load
2103extensions' config files in alphabetical order.
2104Takes no arguments.
2105
2106=cut
2107
2108sub LoadConfigs {
2109    my $self    = shift;
2110
2111    $self->LoadConfig( File => 'RT_Config.pm' );
2112
2113    my @configs = $self->Configs;
2114    $self->LoadConfig( File => $_ ) foreach @configs;
2115    return;
2116}
2117
2118=head1 LoadConfig
2119
2120Takes param hash with C<File> field.
2121First, the site configuration file is loaded, in order to establish
2122overall site settings like hostname and name of RT instance.
2123Then, the core configuration file is loaded to set fallback values
2124for all settings; it bases some values on settings from the site
2125configuration file.
2126
2127B<Note> that core config file don't change options if site config
2128has set them so to add value to some option instead of
2129overriding you have to copy original value from core config file.
2130
2131=cut
2132
2133sub LoadConfig {
2134    my $self = shift;
2135    my %args = ( File => '', @_ );
2136    $args{'File'} =~ s/(?<!Site)(?=Config\.pm$)/Site/;
2137    if ( $args{'File'} eq 'RT_SiteConfig.pm' ) {
2138        my $load = $ENV{RT_SITE_CONFIG} || $args{'File'};
2139        $self->_LoadConfig( %args, File => $load );
2140        # to allow load siteconfig again and again in case it's updated
2141        delete $INC{$load};
2142
2143        my $dir = $ENV{RT_SITE_CONFIG_DIR} || "$RT::EtcPath/RT_SiteConfig.d";
2144        my $localdir = $ENV{RT_SITE_CONFIG_DIR} || "$RT::LocalEtcPath/RT_SiteConfig.d";
2145        for my $file ( sort(<$dir/*.pm>), sort(<$localdir/*.pm>) ) {
2146            $self->_LoadConfig( %args, File => $file, Site => 1, Extension => '' );
2147            delete $INC{$file};
2148        }
2149    }
2150    else {
2151        $self->_LoadConfig(%args);
2152        delete $INC{$args{'File'}};
2153    }
2154
2155    $args{'File'} =~ s/Site(?=Config\.pm$)//;
2156    $self->_LoadConfig(%args);
2157    return 1;
2158}
2159
2160sub _LoadConfig {
2161    my $self = shift;
2162    my %args = ( File => '', @_ );
2163
2164    my ($is_ext, $is_site);
2165    if ( defined $args{Site} && defined $args{Extension} ) {
2166        $is_ext = $args{Extension};
2167        $is_site = $args{Site};
2168    }
2169    elsif ( $args{'File'} eq ($ENV{RT_SITE_CONFIG}||'') ) {
2170        ($is_ext, $is_site) = ('', 1);
2171    } else {
2172        $is_ext = $args{'File'} =~ /^(?!RT_)(?:(.*)_)(?:Site)?Config/ ? $1 : '';
2173        $is_site = $args{'File'} =~ /SiteConfig/ ? 1 : 0;
2174    }
2175
2176    eval {
2177        package RT;
2178        local *Set = sub(\[$@%]@) {
2179            my ( $opt_ref, @args ) = @_;
2180            my ( $pack, $file, $line ) = caller;
2181            return $self->SetFromConfig(
2182                Option     => $opt_ref,
2183                Value      => [@args],
2184                Package    => $pack,
2185                File       => $file,
2186                Line       => $line,
2187                SiteConfig => $is_site,
2188                Extension  => $is_ext,
2189            );
2190        };
2191        local *Plugin = sub {
2192            my (@new_plugins) = @_;
2193            @new_plugins = map {s/-/::/g if not /:/; $_} @new_plugins;
2194            my ( $pack, $file, $line ) = caller;
2195            return $self->SetFromConfig(
2196                Option     => \@RT::Plugins,
2197                Value      => [@RT::Plugins, @new_plugins],
2198                Package    => $pack,
2199                File       => $file,
2200                Line       => $line,
2201                SiteConfig => $is_site,
2202                Extension  => $is_ext,
2203            );
2204        };
2205        my @etc_dirs = ($RT::LocalEtcPath);
2206        push @etc_dirs, RT->PluginDirs('etc') if $is_ext;
2207        push @etc_dirs, $RT::EtcPath, @INC;
2208        local @INC = @etc_dirs;
2209        eval { require $args{'File'} };
2210        if ( $@ && $@ !~ /did not return a true value/ ) {
2211            die $@;
2212        }
2213    };
2214    if ($@) {
2215
2216        if ( $is_site && $@ =~ /^Can't locate \Q$args{File}/ ) {
2217
2218            # Since perl 5.18, the "Can't locate ..." error message contains
2219            # more details. warn to help debug if there is a permission issue.
2220            warn qq{Couldn't load RT config file $args{'File'}:\n\n$@} if $@ =~ /Permission denied at/;
2221            return 1;
2222        }
2223
2224        if ( $is_site || $@ !~ /^Can't locate \Q$args{File}/ ) {
2225            die qq{Couldn't load RT config file $args{'File'}:\n\n$@};
2226        }
2227
2228        my $username = getpwuid($>);
2229        my $group    = getgrgid($();
2230
2231        my ( $file_path, $fileuid, $filegid );
2232        foreach ( $RT::LocalEtcPath, $RT::EtcPath, @INC ) {
2233            my $tmp = File::Spec->catfile( $_, $args{File} );
2234            ( $fileuid, $filegid ) = ( stat($tmp) )[ 4, 5 ];
2235            if ( defined $fileuid ) {
2236                $file_path = $tmp;
2237                last;
2238            }
2239        }
2240        unless ($file_path) {
2241            die
2242                qq{Couldn't load RT config file $args{'File'} as user $username / group $group.\n}
2243                . qq{The file couldn't be found in $RT::LocalEtcPath and $RT::EtcPath.\n$@};
2244        }
2245
2246        my $message = <<EOF;
2247
2248RT couldn't load RT config file %s as:
2249    user: $username
2250    group: $group
2251
2252The file is owned by user %s and group %s.
2253
2254This usually means that the user/group your webserver is running
2255as cannot read the file.  Be careful not to make the permissions
2256on this file too liberal, because it contains database passwords.
2257You may need to put the webserver user in the appropriate group
2258(%s) or change permissions be able to run succesfully.
2259EOF
2260
2261        my $fileusername = getpwuid($fileuid);
2262        my $filegroup    = getgrgid($filegid);
2263        my $errormessage = sprintf( $message,
2264            $file_path, $fileusername, $filegroup, $filegroup );
2265        die "$errormessage\n$@";
2266    } else {
2267        # Loaded successfully
2268        push @LOADED_CONFIGS, {
2269            as          => $args{'File'},
2270            filename    => $INC{ $args{'File'} },
2271            extension   => $is_ext,
2272            site        => $is_site,
2273        };
2274    }
2275    return 1;
2276}
2277
2278sub PostLoadCheck {
2279    my $self = shift;
2280    foreach my $o ( grep $META{$_}{'PostLoadCheck'}, $self->Options( Overridable => undef ) ) {
2281        $META{$o}->{'PostLoadCheck'}->( $self, $self->Get($o) );
2282    }
2283}
2284
2285=head2 SectionMap
2286
2287A data structure used to breakup the option list into tabs/sections/subsections/options
2288This is done by parsing RT_Config.pm.
2289
2290=cut
2291
2292our $SectionMap = [];
2293our $SectionMapLoaded = 0;    # so we only load it once
2294
2295sub LoadSectionMap {
2296    my $self = shift;
2297
2298    if ($SectionMapLoaded) {
2299        return $SectionMap;
2300    }
2301
2302    my $ConfigFile = "$RT::EtcPath/RT_Config.pm";
2303    require Pod::Simple::HTML;
2304    my $PodParser  = Pod::Simple::HTML->new();
2305
2306    my $html;
2307    $PodParser->output_string( \$html );
2308    $PodParser->parse_file($ConfigFile);
2309
2310    my $has_subsection;
2311    while ( $html =~ m{<(h[123]|dt)\b[^>]*>(.*?)</\1>}sg ) {
2312        my ( $tag, $content ) = ( $1, $2 );
2313        if ( $tag eq 'h1' ) {
2314            my ($title) = $content =~ m{<a class='u'\s*name="[^"]*"\s*>([^<]*)</a>};
2315            next if $title =~ /^(?:NAME|DESCRIPTION)$/;
2316            push @$SectionMap, { Name => $title, Content => [] };
2317        }
2318        elsif (@$SectionMap) {
2319            if ( $tag eq 'h2' ) {
2320                my ($title) = $content =~ m{<a class='u'\s*name="[^"]*"\s*>([^<]*)</a>};
2321                push @{ $SectionMap->[-1]{Content} }, { Name => $title, Content => [] };
2322                $has_subsection = 0;
2323            }
2324            elsif ( $tag eq 'h3' ) {
2325                my ($title) = $content =~ m{<a class='u'\s*name="[^"]*"\s*>([^<]*)</a>};
2326                push @{ $SectionMap->[-1]{Content}[-1]{Content} }, { Name => $title, Content => [] };
2327                $has_subsection ||= 1;
2328            }
2329            else {
2330                # tag is 'dt'
2331                if ( !$has_subsection ) {
2332
2333                    # Create an empty subsection to keep the same data structure
2334                    push @{ $SectionMap->[-1]{Content}[-1]{Content} }, { Name => '', Content => [] };
2335                    $has_subsection = 1;
2336                }
2337
2338                # a single item (dt) can document several options, in separate <code> elements
2339                my ($name) = $content =~ m{name=".([^"]*)"};
2340                $name =~ s{,_.}{-}g;    # e.g. DatabaseHost,_$DatabaseRTHost
2341                while ( $content =~ m{<code>(.)([^<]*)</code>}sg ) {
2342                    my ( $sigil, $option ) = ( $1, $2 );
2343                    next unless $sigil =~ m{[\@\%\$]};    # no sigil => this is a value for a select option
2344                    if ( $META{$option} ) {
2345                        next if $META{$option}{Invisible};
2346                    }
2347                    push @{ $SectionMap->[-1]{Content}[-1]{Content}[-1]{Content} }, { Name => $option, Help => $name };
2348                }
2349            }
2350        }
2351    }
2352
2353    # Remove empty tabs/sections
2354    for my $tab (@$SectionMap) {
2355        for my $section ( @{ $tab->{Content} } ) {
2356            @{ $section->{Content} } = grep { @{ $_->{Content} } } @{ $section->{Content} };
2357        }
2358        @{ $tab->{Content} } = grep { @{ $_->{Content} } } @{ $tab->{Content} };
2359    }
2360    @$SectionMap = grep { @{ $_->{Content} } } @$SectionMap;
2361
2362    $SectionMapLoaded = 1;
2363    return $SectionMap;
2364}
2365
2366=head2 Configs
2367
2368Returns list of config files found in local etc, plugins' etc
2369and main etc directories.
2370
2371=cut
2372
2373sub Configs {
2374    my $self    = shift;
2375
2376    my @configs = ();
2377    foreach my $path ( $RT::LocalEtcPath, RT->PluginDirs('etc'), $RT::EtcPath ) {
2378        my $mask = File::Spec->catfile( $path, "*_Config.pm" );
2379        my @files = glob $mask;
2380        @files = grep !/^RT_Config\.pm$/,
2381            grep $_ && /^\w+_Config\.pm$/,
2382            map { s/^.*[\\\/]//; $_ } @files;
2383        push @configs, sort @files;
2384    }
2385
2386    my %seen;
2387    @configs = grep !$seen{$_}++, @configs;
2388    return @configs;
2389}
2390
2391=head2 LoadedConfigs
2392
2393Returns a list of hashrefs, one for each config file loaded.  The keys of the
2394hashes are:
2395
2396=over 4
2397
2398=item as
2399
2400Name this config file was loaded as (relative filename usually).
2401
2402=item filename
2403
2404The full path and filename.
2405
2406=item extension
2407
2408The "extension" part of the filename.  For example, the file C<RTIR_Config.pm>
2409will have an C<extension> value of C<RTIR>.
2410
2411=item site
2412
2413True if the file is considered a site-level override.  For example, C<site>
2414will be false for C<RT_Config.pm> and true for C<RT_SiteConfig.pm>.
2415
2416=back
2417
2418=cut
2419
2420sub LoadedConfigs {
2421    # Copy to avoid the caller changing our internal data
2422    return map { \%$_ } @LOADED_CONFIGS
2423}
2424
2425=head2 Get
2426
2427Takes name of the option as argument and returns its current value.
2428
2429In the case of a user-overridable option, first checks the user's
2430preferences before looking for site-wide configuration.
2431
2432Returns values from RT_SiteConfig, RT_Config and then the %META hash
2433of configuration variables's "Default" for this config variable,
2434in that order.
2435
2436Returns different things in scalar and array contexts. For scalar
2437options it's not that important, however for arrays and hash it's.
2438In scalar context returns references to arrays and hashes.
2439
2440Use C<scalar> perl's op to force context, especially when you use
2441C<(..., Argument => RT->Config->Get('ArrayOpt'), ...)>
2442as perl's '=>' op doesn't change context of the right hand argument to
2443scalar. Instead use C<(..., Argument => scalar RT->Config->Get('ArrayOpt'), ...)>.
2444
2445It's also important for options that have no default value(no default
2446in F<etc/RT_Config.pm>). If you don't force scalar context then you'll
2447get empty list and all your named args will be messed up. For example
2448C<(arg1 => 1, arg2 => RT->Config->Get('OptionDoesNotExist'), arg3 => 3)>
2449will result in C<(arg1 => 1, arg2 => 'arg3', 3)> what is most probably
2450unexpected, or C<(arg1 => 1, arg2 => RT->Config->Get('ArrayOption'), arg3 => 3)>
2451will result in C<(arg1 => 1, arg2 => 'element of option', 'another_one' => ..., 'arg3', 3)>.
2452
2453=cut
2454
2455sub Get {
2456    my ( $self, $name, $user ) = @_;
2457
2458    my $res;
2459    if ( $user && $user->id && $META{$name}->{'Overridable'} ) {
2460        my $prefs = $user->Preferences($RT::System);
2461        $res = $prefs->{$name} if $prefs;
2462    }
2463    $res = $OPTIONS{$name}           unless defined $res;
2464    $res = $META{$name}->{'Default'} unless defined $res;
2465    return $self->_ReturnValue( $res, $META{$name}->{'Type'} || 'SCALAR' );
2466}
2467
2468=head2 GetObfuscated
2469
2470the same as Get, except it returns Obfuscated value via Obfuscate sub
2471
2472=cut
2473
2474sub GetObfuscated {
2475    my $self = shift;
2476    my ( $name, $user ) = @_;
2477    my $obfuscate = $META{$name}->{Obfuscate};
2478
2479    # we use two Get here is to simplify the logic of the return value
2480    # configs need obfuscation are supposed to be less, so won't be too heavy
2481
2482    return $self->Get(@_) unless $obfuscate;
2483
2484    my $res = Clone::clone( $self->Get( @_ ) );
2485    $res = $obfuscate->( $self, $res, $user && $user->Id ? $user : RT->SystemUser );
2486    return $self->_ReturnValue( $res, $META{$name}->{'Type'} || 'SCALAR' );
2487}
2488
2489=head2 Set
2490
2491Set option's value to new value. Takes name of the option and new value.
2492Returns old value.
2493
2494The new value should be scalar, array or hash depending on type of the option.
2495If the option is not defined in meta or the default RT config then it is of
2496scalar type.
2497
2498=cut
2499
2500sub Set {
2501    my ( $self, $name ) = ( shift, shift );
2502
2503    my $old = $OPTIONS{$name};
2504    my $type = $META{$name}->{'Type'} || 'SCALAR';
2505    if ( $type eq 'ARRAY' ) {
2506        $OPTIONS{$name} = [@_];
2507        { no warnings 'once'; no strict 'refs'; @{"RT::$name"} = (@_); }
2508    } elsif ( $type eq 'HASH' ) {
2509        $OPTIONS{$name} = {@_};
2510        { no warnings 'once'; no strict 'refs'; %{"RT::$name"} = (@_); }
2511    } else {
2512        $OPTIONS{$name} = shift;
2513        {no warnings 'once'; no strict 'refs'; ${"RT::$name"} = $OPTIONS{$name}; }
2514    }
2515    $META{$name}->{'Type'} = $type;
2516    $META{$name}->{'PostSet'}->($self, $OPTIONS{$name}, $old)
2517        if $META{$name}->{'PostSet'};
2518    if ($META{$name}->{'Deprecated'}) {
2519        my %deprecated = %{$META{$name}->{'Deprecated'}};
2520        my $new_var = $deprecated{Instead} || '';
2521        $self->SetFromConfig(
2522            Option => \$new_var,
2523            Value  => [$OPTIONS{$name}],
2524            %{$self->Meta($name)->{'Source'}}
2525        ) if $new_var;
2526        $META{$name}->{'PostLoadCheck'} ||= sub {
2527            RT->Deprecated(
2528                Message => "Configuration option $name is deprecated",
2529                Stack   => 0,
2530                %deprecated,
2531            );
2532        };
2533    }
2534    return $self->_ReturnValue( $old, $type );
2535}
2536
2537sub _ReturnValue {
2538    my ( $self, $res, $type ) = @_;
2539    return $res unless wantarray;
2540
2541    if ( $type eq 'ARRAY' ) {
2542        return @{ $res || [] };
2543    } elsif ( $type eq 'HASH' ) {
2544        return %{ $res || {} };
2545    }
2546    return $res;
2547}
2548
2549sub SetFromConfig {
2550    my $self = shift;
2551    my %args = (
2552        Option     => undef,
2553        Value      => [],
2554        Package    => 'RT',
2555        File       => '',
2556        Line       => 0,
2557        SiteConfig => 1,
2558        Extension  => 0,
2559        @_
2560    );
2561
2562    unless ( $args{'File'} ) {
2563        ( $args{'Package'}, $args{'File'}, $args{'Line'} ) = caller(1);
2564    }
2565
2566    my $opt = $args{'Option'};
2567
2568    my $type;
2569    my $name = Symbol::Global::Name->find($opt);
2570    if ($name) {
2571        $type = ref $opt;
2572        $name =~ s/.*:://;
2573    } else {
2574        $name = $$opt;
2575        $type = $META{$name}->{'Type'} || 'SCALAR';
2576    }
2577
2578    # if option is already set we have to check where
2579    # it comes from and may be ignore it
2580    if ( exists $OPTIONS{$name} ) {
2581        if ( $type eq 'HASH' ) {
2582            $args{'Value'} = [
2583                @{ $args{'Value'} },
2584                @{ $args{'Value'} }%2? (undef) : (),
2585                $self->Get( $name ),
2586            ];
2587        } elsif ( $args{'SiteConfig'} && $args{'Extension'} ) {
2588            # if it's site config of an extension then it can only
2589            # override options that came from its main config
2590            if ( $args{'Extension'} ne $META{$name}->{'Source'}{'Extension'} ) {
2591                my %source = %{ $META{$name}->{'Source'} };
2592                push @PreInitLoggerMessages,
2593                    "Change of config option '$name' at $args{'File'} line $args{'Line'} has been ignored."
2594                    ." This option earlier has been set in $source{'File'} line $source{'Line'}."
2595                    ." To overide this option use ". ($source{'Extension'}||'RT')
2596                    ." site config."
2597                ;
2598                return 1;
2599            }
2600        } elsif ( !$args{'SiteConfig'} && $META{$name}->{'Source'}{'SiteConfig'} ) {
2601            # if it's core config then we can override any option that came from another
2602            # core config, but not site config
2603
2604            my %source = %{ $META{$name}->{'Source'} };
2605            if ( $source{'Extension'} ne $args{'Extension'} ) {
2606                # as a site config is loaded earlier then its base config
2607                # then we warn only on different extensions, for example
2608                # RTIR's options is set in main site config
2609                push @PreInitLoggerMessages,
2610                    "Change of config option '$name' at $args{'File'} line $args{'Line'} has been ignored."
2611                    ." It may be ok, but we want you to be aware."
2612                    ." This option has been set earlier in $source{'File'} line $source{'Line'}."
2613                ;
2614            }
2615
2616            return 1;
2617        }
2618    }
2619
2620    $META{$name}->{'Type'} = $type;
2621    foreach (qw(Package File Line SiteConfig Extension Database)) {
2622        $META{$name}->{'Source'}->{$_} = $args{$_};
2623    }
2624    $self->Set( $name, @{ $args{'Value'} } );
2625
2626    return 1;
2627}
2628
2629=head2 Metadata
2630
2631
2632=head2 Meta
2633
2634=cut
2635
2636sub Meta {
2637    return $META{ $_[1] };
2638}
2639
2640sub Sections {
2641    my $self = shift;
2642    my %seen;
2643    my @sections = sort
2644        grep !$seen{$_}++,
2645        map $_->{'Section'} || 'General',
2646        values %META;
2647    return @sections;
2648}
2649
2650sub Options {
2651    my $self = shift;
2652    my %args = ( Section => undef, Overridable => 1, Sorted => 1, @_ );
2653    my @res  = sort keys %META;
2654
2655    @res = grep( ( $META{$_}->{'Section'} || 'General' ) eq $args{'Section'},
2656        @res
2657    ) if defined $args{'Section'};
2658
2659    if ( defined $args{'Overridable'} ) {
2660        @res
2661            = grep( ( $META{$_}->{'Overridable'} || 0 ) == $args{'Overridable'},
2662            @res );
2663    }
2664
2665    if ( $args{'Sorted'} ) {
2666        @res = sort {
2667            ($META{$a}->{SortOrder}||9999) <=> ($META{$b}->{SortOrder}||9999)
2668            || $a cmp $b
2669        } @res;
2670    } else {
2671        @res = sort { $a cmp $b } @res;
2672    }
2673    return @res;
2674}
2675
2676=head2 AddOption( Name => '', Section => '', ... )
2677
2678=cut
2679
2680sub AddOption {
2681    my $self = shift;
2682    my %args = (
2683        Name            => undef,
2684        Section         => undef,
2685        Overridable     => 0,
2686        SortOrder       => undef,
2687        Widget          => '/Widgets/Form/String',
2688        WidgetArguments => {},
2689        @_
2690    );
2691
2692    unless ( $args{Name} ) {
2693        $RT::Logger->error("Need Name to add a new config");
2694        return;
2695    }
2696
2697    unless ( $args{Section} ) {
2698        $RT::Logger->error("Need Section to add a new config option");
2699        return;
2700    }
2701
2702    $META{ delete $args{Name} } = \%args;
2703}
2704
2705=head2 DeleteOption( Name => '' )
2706
2707=cut
2708
2709sub DeleteOption {
2710    my $self = shift;
2711    my %args = (
2712        Name            => undef,
2713        @_
2714        );
2715    if ( $args{Name} ) {
2716        delete $META{$args{Name}};
2717    }
2718    else {
2719        $RT::Logger->error("Need Name to remove a config option");
2720        return;
2721    }
2722}
2723
2724=head2 UpdateOption( Name => '' ), Section => '', ... )
2725
2726=cut
2727
2728sub UpdateOption {
2729    my $self = shift;
2730    my %args = (
2731        Name            => undef,
2732        Section         => undef,
2733        Overridable     => undef,
2734        SortOrder       => undef,
2735        Widget          => undef,
2736        WidgetArguments => undef,
2737        @_
2738    );
2739
2740    my $name = delete $args{Name};
2741
2742    unless ( $name ) {
2743        $RT::Logger->error("Need Name to update a new config");
2744        return;
2745    }
2746
2747    unless ( exists $META{$name} ) {
2748        $RT::Logger->error("Config $name doesn't exist");
2749        return;
2750    }
2751
2752    for my $type ( keys %args ) {
2753        next unless defined $args{$type};
2754        $META{$name}{$type} = $args{$type};
2755    }
2756    return 1;
2757}
2758
2759sub ObjectHasCustomFieldGrouping {
2760    my $self        = shift;
2761    my %args        = ( Object => undef, Grouping => undef, @_ );
2762    my $object_type = RT::CustomField->_GroupingClass($args{Object});
2763    my $groupings   = RT->Config->Get( 'CustomFieldGroupings' );
2764    return 0 unless $groupings;
2765    return 1 if $groupings->{$object_type} && grep { $_ eq $args{Grouping} } @{ $groupings->{$object_type} };
2766    return 0;
2767}
2768
2769# Internal method to activate ExtneralAuth if any ExternalAuth config
2770# options are set.
2771sub EnableExternalAuth {
2772    my $self = shift;
2773
2774    $self->Set('ExternalAuth', 1);
2775    require RT::Authen::ExternalAuth;
2776    return;
2777}
2778
2779my $database_config_cache_time = 0;
2780my %original_setting_from_files;
2781my $in_config_change_txn = 0;
2782
2783sub BeginDatabaseConfigChanges {
2784    $in_config_change_txn = $in_config_change_txn + 1;
2785}
2786
2787sub EndDatabaseConfigChanges {
2788    $in_config_change_txn = $in_config_change_txn - 1;
2789    if (!$in_config_change_txn) {
2790        shift->ApplyConfigChangeToAllServerProcesses();
2791    }
2792}
2793
2794sub ApplyConfigChangeToAllServerProcesses {
2795    my $self = shift;
2796
2797    return if $in_config_change_txn;
2798
2799    # first apply locally
2800    $self->LoadConfigFromDatabase();
2801
2802    # then notify other servers
2803    RT->System->ConfigCacheNeedsUpdate($database_config_cache_time);
2804}
2805
2806sub RefreshConfigFromDatabase {
2807    my $self = shift;
2808    if ($in_config_change_txn) {
2809        RT->Logger->error("It appears that there were unbalanced calls to BeginDatabaseConfigChanges with EndDatabaseConfigChanges; this indicates a software fault");
2810        $in_config_change_txn = 0;
2811    }
2812
2813    if( RT->InstallMode ) { return; } # RT can't load the config in the DB if the DB is not there!
2814    my $needs_update = RT->System->ConfigCacheNeedsUpdate;
2815    if ($needs_update > $database_config_cache_time) {
2816        $self->LoadConfigFromDatabase();
2817        $HTML::Mason::Commands::ReloadScrubber = 1;
2818        $database_config_cache_time = $needs_update;
2819    }
2820}
2821
2822sub LoadConfigFromDatabase {
2823    my $self = shift;
2824
2825    $database_config_cache_time = time;
2826
2827    my $settings = RT::Configurations->new(RT->SystemUser);
2828    $settings->LimitToEnabled;
2829
2830    my %seen;
2831
2832    while (my $setting = $settings->Next) {
2833        my $name = $setting->Name;
2834        my ($value, $error) = $setting->DecodedContent;
2835        next if $error;
2836
2837        if (!exists $original_setting_from_files{$name}) {
2838            $original_setting_from_files{$name} = [
2839                scalar($self->Get($name)),
2840                Clone::clone(scalar($self->Meta($name))),
2841            ];
2842        }
2843
2844        $seen{$name}++;
2845
2846        # are we inadvertantly overriding RT_SiteConfig.pm?
2847        my $meta = $META{$name};
2848        if ($meta->{'Source'}) {
2849            my %source = %{ $meta->{'Source'} };
2850            if ($source{'SiteConfig'} && $source{'File'} ne 'database') {
2851                push @PreInitLoggerMessages,
2852                    "Change of config option '$name' at $source{File} line $source{Line} has been overridden by the config setting from the database. "
2853                    ."Please remove it from $source{File} or from the database to avoid confusion.";
2854            }
2855        }
2856
2857        my $type = $meta->{Type} || 'SCALAR';
2858
2859        my $val = $type eq 'ARRAY' ? $value
2860                : $type eq 'HASH'  ? [ %$value ]
2861                                   : [ $value ];
2862
2863        # hashes combine, but by default previous config settings shadow
2864        # later changes, here we want database configs to shadow file ones.
2865        if ($type eq 'HASH') {
2866            $val = [ $self->Get($name), @$val ];
2867            $self->Set($name, ());
2868        }
2869
2870        $self->SetFromConfig(
2871            Option     => \$name,
2872            Value      => $val,
2873            Package    => 'N/A',
2874            File       => 'database',
2875            Line       => 'N/A',
2876            Database   => 1,
2877            SiteConfig => 1,
2878        );
2879    }
2880
2881    # anything that wasn't loaded from the database but has been set in
2882    # %original_setting_from_files must have been disabled from the database,
2883    # so we want to restore the original setting
2884    for my $name (keys %original_setting_from_files) {
2885        next if $seen{$name};
2886
2887        my ($value, $meta) = @{ $original_setting_from_files{$name} };
2888        my $type = $meta->{Type} || 'SCALAR';
2889
2890        if ($type eq 'ARRAY') {
2891            $self->Set($name, @$value);
2892        }
2893        elsif ($type eq 'HASH') {
2894            $self->Set($name, %$value);
2895        }
2896        else {
2897            $self->Set($name, $value);
2898        }
2899
2900        %{ $META{$name} } = %$meta;
2901    }
2902}
2903
2904sub _GetFromFilesOnly {
2905    my ( $self, $name ) = @_;
2906    return $original_setting_from_files{$name} ? $original_setting_from_files{$name}[0] : undef;
2907}
2908
2909RT::Base->_ImportOverlays();
2910
29111;
2912