a6b3269949eabcb619059df87482664fbcee3e4b
[usit-rt.git] / lib / RT / Config.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2014 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
49 package RT::Config;
50
51 use strict;
52 use warnings;
53
54 use File::Spec ();
55 use Symbol::Global::Name;
56
57 =head1 NAME
58
59     RT::Config - RT's config
60
61 =head1 SYNOPSYS
62
63     # get config object
64     use RT::Config;
65     my $config = RT::Config->new;
66     $config->LoadConfigs;
67
68     # get or set option
69     my $rt_web_path = $config->Get('WebPath');
70     $config->Set(EmailOutputEncoding => 'latin1');
71
72     # get config object from RT package
73     use RT;
74     RT->LoadConfig;
75     my $config = RT->Config;
76
77 =head1 DESCRIPTION
78
79 C<RT::Config> class provide access to RT's and RT extensions' config files.
80
81 RT uses two files for site configuring:
82
83 First file is F<RT_Config.pm> - core config file. This file is shipped
84 with RT distribution and contains default values for all available options.
85 B<You should never edit this file.>
86
87 Second file is F<RT_SiteConfig.pm> - site config file. You can use it
88 to customize your RT instance. In this file you can override any option
89 listed in core config file.
90
91 RT extensions could also provide thier config files. Extensions should
92 use F<< <NAME>_Config.pm >> and F<< <NAME>_SiteConfig.pm >> names for
93 config files, where <NAME> is extension name.
94
95 B<NOTE>: All options from RT's config and extensions' configs are saved
96 in one place and thus extension could override RT's options, but it is not
97 recommended.
98
99 =cut
100
101 =head2 %META
102
103 Hash of Config options that may be user overridable
104 or may require more logic than should live in RT_*Config.pm
105
106 Keyed by config name, there are several properties that
107 can be set for each config optin:
108
109  Section     - What header this option should be grouped
110                under on the user Preferences page
111  Overridable - Can users change this option
112  SortOrder   - Within a Section, how should the options be sorted
113                for display to the user
114  Widget      - Mason component path to widget that should be used 
115                to display this config option
116  WidgetArguments - An argument hash passed to the WIdget
117     Description - Friendly description to show the user
118     Values      - Arrayref of options (for select Widget)
119     ValuesLabel - Hashref, key is the Value from the Values
120                   list, value is a user friendly description
121                   of the value
122     Callback    - subref that receives no arguments.  It returns
123                   a hashref of items that are added to the rest
124                   of the WidgetArguments
125  PostSet       - subref passed the RT::Config object and the current and
126                  previous setting of the config option.  This is called well
127                  before much of RT's subsystems are initialized, so what you
128                  can do here is pretty limited.  It's mostly useful for
129                  effecting the value of other config options early.
130  PostLoadCheck - subref passed the RT::Config object and the current
131                  setting of the config option.  Can make further checks
132                  (such as seeing if a library is installed) and then change
133                  the setting of this or other options in the Config using 
134                  the RT::Config option.
135    Obfuscate   - subref passed the RT::Config object, current setting of the config option
136                  and a user object, can return obfuscated value. it's called in
137                  RT->Config->GetObfuscated() 
138
139 =cut
140
141 our %META;
142 %META = (
143     # General user overridable options
144     DefaultQueue => {
145         Section         => 'General',
146         Overridable     => 1,
147         SortOrder       => 1,
148         Widget          => '/Widgets/Form/Select',
149         WidgetArguments => {
150             Description => 'Default queue',    #loc
151             Callback    => sub {
152                 my $ret = { Values => [], ValuesLabel => {}};
153                 my $q = RT::Queues->new($HTML::Mason::Commands::session{'CurrentUser'});
154                 $q->UnLimit;
155                 while (my $queue = $q->Next) {
156                     next unless $queue->CurrentUserHasRight("CreateTicket");
157                     push @{$ret->{Values}}, $queue->Id;
158                     $ret->{ValuesLabel}{$queue->Id} = $queue->Name;
159                 }
160                 return $ret;
161             },
162         }
163     },
164     RememberDefaultQueue => {
165         Section     => 'General',
166         Overridable => 1,
167         SortOrder   => 2,
168         Widget      => '/Widgets/Form/Boolean',
169         WidgetArguments => {
170             Description => 'Remember default queue' # loc
171         }
172     },
173     UsernameFormat => {
174         Section         => 'General',
175         Overridable     => 1,
176         SortOrder       => 3,
177         Widget          => '/Widgets/Form/Select',
178         WidgetArguments => {
179             Description => 'Username format', # loc
180             Values      => [qw(concise verbose)],
181             ValuesLabel => {
182                 concise => 'Short usernames', # loc
183                 verbose => 'Name and email address', # loc
184             },
185         },
186     },
187     AutocompleteOwners => {
188         Section     => 'General',
189         Overridable => 1,
190         SortOrder   => 3.1,
191         Widget      => '/Widgets/Form/Boolean',
192         WidgetArguments => {
193             Description => 'Use autocomplete to find owners?', # loc
194             Hints       => 'Replaces the owner dropdowns with textboxes' #loc
195         }
196     },
197     WebDefaultStylesheet => {
198         Section         => 'General',                #loc
199         Overridable     => 1,
200         SortOrder       => 4,
201         Widget          => '/Widgets/Form/Select',
202         WidgetArguments => {
203             Description => 'Theme',                  #loc
204             # XXX: we need support for 'get values callback'
205             Values => [qw(rudder web2 aileron ballard)],
206         },
207         PostLoadCheck => sub {
208             my $self = shift;
209             my $value = $self->Get('WebDefaultStylesheet');
210
211             my @roots = RT::Interface::Web->StaticRoots;
212             for my $root (@roots) {
213                 return if -d "$root/css/$value";
214             }
215
216             $RT::Logger->warning(
217                 "The default stylesheet ($value) does not exist in this instance of RT. "
218               . "Defaulting to aileron."
219             );
220
221             $self->Set('WebDefaultStylesheet', 'aileron');
222         },
223     },
224     TimeInICal => {
225         Section     => 'General',
226         Overridable => 1,
227         SortOrder   => 5,
228         Widget      => '/Widgets/Form/Boolean',
229         WidgetArguments => {
230             Description => 'Include time in iCal feed events?', # loc
231             Hints       => 'Formats iCal feed events with date and time' #loc
232         }
233     },
234     UseSideBySideLayout => {
235         Section => 'Ticket composition',
236         Overridable => 1,
237         SortOrder => 5,
238         Widget => '/Widgets/Form/Boolean',
239         WidgetArguments => {
240             Description => 'Use a two column layout for create and update forms?' # loc
241         }
242     },
243     MessageBoxRichText => {
244         Section => 'Ticket composition',
245         Overridable => 1,
246         SortOrder => 5.1,
247         Widget => '/Widgets/Form/Boolean',
248         WidgetArguments => {
249             Description => 'WYSIWYG message composer' # loc
250         }
251     },
252     MessageBoxRichTextHeight => {
253         Section => 'Ticket composition',
254         Overridable => 1,
255         SortOrder => 6,
256         Widget => '/Widgets/Form/Integer',
257         WidgetArguments => {
258             Description => 'WYSIWYG composer height', # loc
259         }
260     },
261     MessageBoxWidth => {
262         Section         => 'Ticket composition',
263         Overridable     => 1,
264         SortOrder       => 7,
265         Widget          => '/Widgets/Form/Integer',
266         WidgetArguments => {
267             Description => 'Message box width',           #loc
268         },
269     },
270     MessageBoxHeight => {
271         Section         => 'Ticket composition',
272         Overridable     => 1,
273         SortOrder       => 8,
274         Widget          => '/Widgets/Form/Integer',
275         WidgetArguments => {
276             Description => 'Message box height',          #loc
277         },
278     },
279     DefaultTimeUnitsToHours => {
280         Section         => 'Ticket composition', #loc
281         Overridable     => 1,
282         SortOrder       => 9,
283         Widget          => '/Widgets/Form/Boolean',
284         WidgetArguments => {
285             Description => 'Enter time in hours by default', #loc
286             Hints       => 'Only for entry, not display', #loc
287         },
288     },
289     SearchResultsRefreshInterval => {
290         Section         => 'General',                       #loc
291         Overridable     => 1,
292         SortOrder       => 9,
293         Widget          => '/Widgets/Form/Select',
294         WidgetArguments => {
295             Description => 'Search results refresh interval',                            #loc
296             Values      => [qw(0 120 300 600 1200 3600 7200)],
297             ValuesLabel => {
298                 0 => "Don't refresh search results.",                      #loc
299                 120 => "Refresh search results every 2 minutes.",          #loc
300                 300 => "Refresh search results every 5 minutes.",          #loc
301                 600 => "Refresh search results every 10 minutes.",         #loc
302                 1200 => "Refresh search results every 20 minutes.",        #loc
303                 3600 => "Refresh search results every 60 minutes.",        #loc
304                 7200 => "Refresh search results every 120 minutes.",       #loc
305             },  
306         },  
307     },
308
309     # User overridable options for RT at a glance
310     HomePageRefreshInterval => {
311         Section         => 'RT at a glance',                       #loc
312         Overridable     => 1,
313         SortOrder       => 2,
314         Widget          => '/Widgets/Form/Select',
315         WidgetArguments => {
316             Description => 'Home page refresh interval',                #loc
317             Values      => [qw(0 120 300 600 1200 3600 7200)],
318             ValuesLabel => {
319                 0 => "Don't refresh home page.",                  #loc
320                 120 => "Refresh home page every 2 minutes.",      #loc
321                 300 => "Refresh home page every 5 minutes.",      #loc
322                 600 => "Refresh home page every 10 minutes.",     #loc
323                 1200 => "Refresh home page every 20 minutes.",    #loc
324                 3600 => "Refresh home page every 60 minutes.",    #loc
325                 7200 => "Refresh home page every 120 minutes.",   #loc
326             },  
327         },  
328     },
329
330     # User overridable options for Ticket displays
331     PreferRichText => {
332         Section         => 'Ticket display', # loc
333         Overridable     => 1,
334         SortOrder       => 0.9,
335         Widget          => '/Widgets/Form/Boolean',
336         WidgetArguments => {
337             Description => 'Display messages in rich text if available', # loc
338             Hints       => 'Rich text (HTML) shows formatting such as colored text, bold, italics, and more', # loc
339         },
340     },
341     MaxInlineBody => {
342         Section         => 'Ticket display',              #loc
343         Overridable     => 1,
344         SortOrder       => 1,
345         Widget          => '/Widgets/Form/Integer',
346         WidgetArguments => {
347             Description => 'Maximum inline message length',    #loc
348             Hints =>
349             "Length in characters; Use '0' to show all messages inline, regardless of length" #loc
350         },
351     },
352     OldestTransactionsFirst => {
353         Section         => 'Ticket display',
354         Overridable     => 1,
355         SortOrder       => 2,
356         Widget          => '/Widgets/Form/Boolean',
357         WidgetArguments => {
358             Description => 'Show oldest history first',    #loc
359         },
360     },
361     ShowHistory => {
362         Section         => 'Ticket display',
363         Overridable     => 1,
364         SortOrder       => 3,
365         Widget          => '/Widgets/Form/Select',
366         WidgetArguments => {
367             Description => 'Show history',                #loc
368             Values      => [qw(delay click always)],
369             ValuesLabel => {
370                 delay   => "after the rest of the page loads",  #loc
371                 click   => "after clicking a link",             #loc
372                 always  => "immediately",                       #loc
373             },
374         },
375     },
376     ShowUnreadMessageNotifications => { 
377         Section         => 'Ticket display',
378         Overridable     => 1,
379         SortOrder       => 4,
380         Widget          => '/Widgets/Form/Boolean',
381         WidgetArguments => {
382             Description => 'Notify me of unread messages',    #loc
383         },
384
385     },
386     PlainTextPre => {
387         PostSet => sub {
388             my $self  = shift;
389             my $value = shift;
390             $self->SetFromConfig(
391                 Option => \'PlainTextMono',
392                 Value  => [$value],
393                 %{$self->Meta('PlainTextPre')->{'Source'}}
394             );
395         },
396         PostLoadCheck => sub {
397             my $self = shift;
398             # XXX: deprecated, remove in 4.4
399             $RT::Logger->info("You set \$PlainTextPre in your config, which has been removed in favor of \$PlainTextMono.  Please update your config.")
400                 if $self->Meta('PlainTextPre')->{'Source'}{'Package'};
401         },
402     },
403     PlainTextMono => {
404         Section         => 'Ticket display',
405         Overridable     => 1,
406         SortOrder       => 5,
407         Widget          => '/Widgets/Form/Boolean',
408         WidgetArguments => {
409             Description => 'Display plain-text attachments in fixed-width font', #loc
410             Hints => 'Display all plain-text attachments in a monospace font with formatting preserved, but wrapping as needed.', #loc
411         },
412     },
413     MoreAboutRequestorTicketList => {
414         Section         => 'Ticket display',                       #loc
415         Overridable     => 1,
416         SortOrder       => 6,
417         Widget          => '/Widgets/Form/Select',
418         WidgetArguments => {
419             Description => 'What tickets to display in the "More about requestor" box',                #loc
420             Values      => [qw(Active Inactive All None)],
421             ValuesLabel => {
422                 Active   => "Show the Requestor's 10 highest priority active tickets",                  #loc
423                 Inactive => "Show the Requestor's 10 highest priority inactive tickets",      #loc
424                 All      => "Show the Requestor's 10 highest priority tickets",      #loc
425                 None     => "Show no tickets for the Requestor", #loc
426             },
427         },
428     },
429     SimplifiedRecipients => {
430         Section         => 'Ticket display',                       #loc
431         Overridable     => 1,
432         SortOrder       => 7,
433         Widget          => '/Widgets/Form/Boolean',
434         WidgetArguments => {
435             Description => "Show simplified recipient list on ticket update",                #loc
436         },
437     },
438     DisplayTicketAfterQuickCreate => {
439         Section         => 'Ticket display',
440         Overridable     => 1,
441         SortOrder       => 8,
442         Widget          => '/Widgets/Form/Boolean',
443         WidgetArguments => {
444             Description => 'Display ticket after "Quick Create"', #loc
445         },
446     },
447     QuoteFolding => {
448         Section => 'Ticket display',
449         Overridable => 1,
450         SortOrder => 9,
451         Widget => '/Widgets/Form/Boolean',
452         WidgetArguments => {
453             Description => 'Enable quote folding?' # loc
454         }
455     },
456
457     # User overridable locale options
458     DateTimeFormat => {
459         Section         => 'Locale',                       #loc
460         Overridable     => 1,
461         Widget          => '/Widgets/Form/Select',
462         WidgetArguments => {
463             Description => 'Date format',                            #loc
464             Callback => sub { my $ret = { Values => [], ValuesLabel => {}};
465                               my $date = RT::Date->new($HTML::Mason::Commands::session{'CurrentUser'});
466                               $date->SetToNow;
467                               foreach my $value ($date->Formatters) {
468                                  push @{$ret->{Values}}, $value;
469                                  $ret->{ValuesLabel}{$value} = $date->Get(
470                                      Format     => $value,
471                                      Timezone   => 'user',
472                                  );
473                               }
474                               return $ret;
475             },
476         },
477     },
478
479     RTAddressRegexp => {
480         Type    => 'SCALAR',
481         PostLoadCheck => sub {
482             my $self = shift;
483             my $value = $self->Get('RTAddressRegexp');
484             if (not $value) {
485                 $RT::Logger->debug(
486                     'The RTAddressRegexp option is not set in the config.'
487                     .' Not setting this option results in additional SQL queries to'
488                     .' check whether each address belongs to RT or not.'
489                     .' It is especially important to set this option if RT recieves'
490                     .' emails on addresses that are not in the database or config.'
491                 );
492             } elsif (ref $value and ref $value eq "Regexp") {
493                 # Ensure that the regex is case-insensitive; while the
494                 # local part of email addresses is _technically_
495                 # case-sensitive, most MTAs don't treat it as such.
496                 $RT::Logger->warning(
497                     'RTAddressRegexp is set to a case-sensitive regular expression.'
498                     .' This may lead to mail loops with MTAs which treat the'
499                     .' local part as case-insensitive -- which is most of them.'
500                 ) if "$value" =~ /^\(\?[a-z]*-([a-z]*):/ and "$1" =~ /i/;
501             }
502         },
503     },
504     # User overridable mail options
505     EmailFrequency => {
506         Section         => 'Mail',                                     #loc
507         Overridable     => 1,
508         Default     => 'Individual messages',
509         Widget          => '/Widgets/Form/Select',
510         WidgetArguments => {
511             Description => 'Email delivery',    #loc
512             Values      => [
513             'Individual messages',    #loc
514             'Daily digest',           #loc
515             'Weekly digest',          #loc
516             'Suspended'               #loc
517             ]
518         }
519     },
520     NotifyActor => {
521         Section         => 'Mail',                                     #loc
522         Overridable     => 1,
523         SortOrder       => 2,
524         Widget          => '/Widgets/Form/Boolean',
525         WidgetArguments => {
526             Description => 'Outgoing mail', #loc
527             Hints => 'Should RT send you mail for ticket updates you make?', #loc
528         }
529     },
530
531     # this tends to break extensions that stash links in ticket update pages
532     Organization => {
533         Type            => 'SCALAR',
534         PostLoadCheck   => sub {
535             my ($self,$value) = @_;
536             $RT::Logger->error("your \$Organization setting ($value) appears to contain whitespace.  Please fix this.")
537                 if $value =~ /\s/;;
538         },
539     },
540
541     # Internal config options
542     FullTextSearch => {
543         Type => 'HASH',
544         PostLoadCheck => sub {
545             my $self = shift;
546             my $v = $self->Get('FullTextSearch');
547             return unless $v->{Enable} and $v->{Indexed};
548             my $dbtype = $self->Get('DatabaseType');
549             if ($dbtype eq 'Oracle') {
550                 if (not $v->{IndexName}) {
551                     $RT::Logger->error("No IndexName set for full-text index; disabling");
552                     $v->{Enable} = $v->{Indexed} = 0;
553                 }
554             } elsif ($dbtype eq 'Pg') {
555                 my $bad = 0;
556                 if (not $v->{'Column'}) {
557                     $RT::Logger->error("No Column set for full-text index; disabling");
558                     $v->{Enable} = $v->{Indexed} = 0;
559                 } elsif ($v->{'Column'} eq "Content"
560                              and (not $v->{'Table'} or $v->{'Table'} eq "Attachments")) {
561                     $RT::Logger->error("Column for full-text index is set to Content, not tsvector column; disabling");
562                     $v->{Enable} = $v->{Indexed} = 0;
563                 }
564             } elsif ($dbtype eq 'mysql') {
565                 if (not $v->{'Table'}) {
566                     $RT::Logger->error("No Table set for full-text index; disabling");
567                     $v->{Enable} = $v->{Indexed} = 0;
568                 } elsif ($v->{'Table'} eq "Attachments") {
569                     $RT::Logger->error("Table for full-text index is set to Attachments, not SphinxSE table; disabling");
570                     $v->{Enable} = $v->{Indexed} = 0;
571                 } elsif (not $v->{'MaxMatches'}) {
572                     $RT::Logger->warn("No MaxMatches set for full-text index; defaulting to 10000");
573                     $v->{MaxMatches} = 10_000;
574                 }
575             } else {
576                 $RT::Logger->error("Indexed full-text-search not supported for $dbtype");
577                 $v->{Indexed} = 0;
578             }
579         },
580     },
581     DisableGraphViz => {
582         Type            => 'SCALAR',
583         PostLoadCheck   => sub {
584             my $self  = shift;
585             my $value = shift;
586             return if $value;
587             return if $INC{'GraphViz.pm'};
588             local $@;
589             return if eval {require GraphViz; 1};
590             $RT::Logger->debug("You've enabled GraphViz, but we couldn't load the module: $@");
591             $self->Set( DisableGraphViz => 1 );
592         },
593     },
594     DisableGD => {
595         Type            => 'SCALAR',
596         PostLoadCheck   => sub {
597             my $self  = shift;
598             my $value = shift;
599             return if $value;
600             return if $INC{'GD.pm'};
601             local $@;
602             return if eval {require GD; 1};
603             $RT::Logger->debug("You've enabled GD, but we couldn't load the module: $@");
604             $self->Set( DisableGD => 1 );
605         },
606     },
607     MailCommand => {
608         Type    => 'SCALAR',
609         PostLoadCheck => sub {
610             my $self = shift;
611             my $value = $self->Get('MailCommand');
612             return if ref($value) eq "CODE"
613                 or $value =~/^(sendmail|sendmailpipe|qmail|testfile)$/;
614             $RT::Logger->error("Unknown value for \$MailCommand: $value; defaulting to sendmailpipe");
615             $self->Set( MailCommand => 'sendmailpipe' );
616         },
617     },
618     MailPlugins  => {
619         Type => 'ARRAY',
620         PostLoadCheck => sub {
621             my $self = shift;
622             my @plugins = $self->Get('MailPlugins');
623             if ( grep $_ eq 'Auth::GnuPG' || $_ eq 'Auth::SMIME', @plugins ) {
624                 $RT::Logger->warning(
625                     'Auth::GnuPG and Auth::SMIME (from an extension) have been'
626                     .' replaced with Auth::Crypt.  @MailPlugins has been adjusted,'
627                     .' but should be updated to replace both with Auth::Crypt to'
628                     .' silence this warning.'
629                 );
630                 my %seen;
631                 @plugins =
632                     grep !$seen{$_}++,
633                     grep {
634                         $_ eq 'Auth::GnuPG' || $_ eq 'Auth::SMIME'
635                         ? 'Auth::Crypt' : $_
636                     } @plugins;
637                 $self->Set( MailPlugins => @plugins );
638             }
639         },
640     },
641     Crypt        => {
642         Type => 'HASH',
643         PostLoadCheck => sub {
644             my $self = shift;
645             require RT::Crypt;
646
647             for my $proto (RT::Crypt->EnabledProtocols) {
648                 my $opt = $self->Get($proto);
649                 if (not RT::Crypt->LoadImplementation($proto)) {
650                     $RT::Logger->error("You enabled $proto, but we couldn't load module RT::Crypt::$proto");
651                     $opt->{'Enable'} = 0;
652                 } elsif (not RT::Crypt->LoadImplementation($proto)->Probe) {
653                     $opt->{'Enable'} = 0;
654                 } elsif ($META{$proto}{'PostLoadCheck'}) {
655                     $META{$proto}{'PostLoadCheck'}->( $self, $self->Get( $proto ) );
656                 }
657
658             }
659
660             my $opt = $self->Get('Crypt');
661             my @enabled = RT::Crypt->EnabledProtocols;
662             my %enabled;
663             $enabled{$_} = 1 for @enabled;
664             $opt->{'Enable'} = scalar @enabled;
665             $opt->{'Incoming'} = [ $opt->{'Incoming'} ]
666                 if $opt->{'Incoming'} and not ref $opt->{'Incoming'};
667             if ( $opt->{'Incoming'} && @{ $opt->{'Incoming'} } ) {
668                 $opt->{'Incoming'} = [ grep {$enabled{$_}} @{$opt->{'Incoming'}} ];
669             } else {
670                 $opt->{'Incoming'} = \@enabled;
671             }
672             if ( $opt->{'Outgoing'} ) {
673                 $opt->{'Outgoing'} = $enabled[0] unless $enabled{$opt->{'Outgoing'}};
674             } else {
675                 $opt->{'Outgoing'} = $enabled[0];
676             }
677         },
678     },
679     SMIME        => {
680         Type => 'HASH',
681         PostLoadCheck => sub {
682             my $self = shift;
683             my $opt = $self->Get('SMIME');
684             return unless $opt->{'Enable'};
685
686             if (exists $opt->{Keyring}) {
687                 unless ( File::Spec->file_name_is_absolute( $opt->{Keyring} ) ) {
688                     $opt->{Keyring} = File::Spec->catfile( $RT::BasePath, $opt->{Keyring} );
689                 }
690                 unless (-d $opt->{Keyring} and -r _) {
691                     $RT::Logger->info(
692                         "RT's SMIME libraries couldn't successfully read your".
693                         " configured SMIME keyring directory (".$opt->{Keyring}
694                         .").");
695                     delete $opt->{Keyring};
696                 }
697             }
698
699             if (defined $opt->{CAPath}) {
700                 if (-d $opt->{CAPath} and -r _) {
701                     # directory, all set
702                 } elsif (-f $opt->{CAPath} and -r _) {
703                     # file, all set
704                 } else {
705                     $RT::Logger->warn(
706                         "RT's SMIME libraries could not read your configured CAPath (".$opt->{CAPath}.")"
707                     );
708                     delete $opt->{CAPath};
709                 }
710             }
711         },
712     },
713     GnuPG        => {
714         Type => 'HASH',
715         PostLoadCheck => sub {
716             my $self = shift;
717             my $gpg = $self->Get('GnuPG');
718             return unless $gpg->{'Enable'};
719
720             my $gpgopts = $self->Get('GnuPGOptions');
721             unless ( File::Spec->file_name_is_absolute( $gpgopts->{homedir} ) ) {
722                 $gpgopts->{homedir} = File::Spec->catfile( $RT::BasePath, $gpgopts->{homedir} );
723             }
724             unless (-d $gpgopts->{homedir}  && -r _ ) { # no homedir, no gpg
725                 $RT::Logger->info(
726                     "RT's GnuPG libraries couldn't successfully read your".
727                     " configured GnuPG home directory (".$gpgopts->{homedir}
728                     ."). GnuPG support has been disabled");
729                 $gpg->{'Enable'} = 0;
730                 return;
731             }
732
733             if ( grep exists $gpg->{$_}, qw(RejectOnMissingPrivateKey RejectOnBadData AllowEncryptDataInDB) ) {
734                 $RT::Logger->warning(
735                     "The RejectOnMissingPrivateKey, RejectOnBadData and AllowEncryptDataInDB"
736                     ." GnuPG options are now properties of the generic Crypt configuration. You"
737                     ." should set them there instead."
738                 );
739                 delete $gpg->{$_} for qw(RejectOnMissingPrivateKey RejectOnBadData AllowEncryptDataInDB);
740             }
741         }
742     },
743     GnuPGOptions => { Type => 'HASH' },
744     ReferrerWhitelist => { Type => 'ARRAY' },
745     WebPath => {
746         PostLoadCheck => sub {
747             my $self  = shift;
748             my $value = shift;
749
750             # "In most cases, you should leave $WebPath set to '' (an empty value)."
751             return unless $value;
752
753             # try to catch someone who assumes that you shouldn't leave this empty
754             if ($value eq '/') {
755                 $RT::Logger->error("For the WebPath config option, use the empty string instead of /");
756                 return;
757             }
758
759             # $WebPath requires a leading / but no trailing /, or it can be blank.
760             return if $value =~ m{^/.+[^/]$};
761
762             if ($value =~ m{/$}) {
763                 $RT::Logger->error("The WebPath config option requires no trailing slash");
764             }
765
766             if ($value !~ m{^/}) {
767                 $RT::Logger->error("The WebPath config option requires a leading slash");
768             }
769         },
770     },
771     WebDomain => {
772         PostLoadCheck => sub {
773             my $self  = shift;
774             my $value = shift;
775
776             if (!$value) {
777                 $RT::Logger->error("You must set the WebDomain config option");
778                 return;
779             }
780
781             if ($value =~ m{^(\w+://)}) {
782                 $RT::Logger->error("The WebDomain config option must not contain a scheme ($1)");
783                 return;
784             }
785
786             if ($value =~ m{(/.*)}) {
787                 $RT::Logger->error("The WebDomain config option must not contain a path ($1)");
788                 return;
789             }
790
791             if ($value =~ m{:(\d*)}) {
792                 $RT::Logger->error("The WebDomain config option must not contain a port ($1)");
793                 return;
794             }
795         },
796     },
797     WebPort => {
798         PostLoadCheck => sub {
799             my $self  = shift;
800             my $value = shift;
801
802             if (!$value) {
803                 $RT::Logger->error("You must set the WebPort config option");
804                 return;
805             }
806
807             if ($value !~ m{^\d+$}) {
808                 $RT::Logger->error("The WebPort config option must be an integer");
809             }
810         },
811     },
812     WebBaseURL => {
813         PostLoadCheck => sub {
814             my $self  = shift;
815             my $value = shift;
816
817             if (!$value) {
818                 $RT::Logger->error("You must set the WebBaseURL config option");
819                 return;
820             }
821
822             if ($value !~ m{^https?://}i) {
823                 $RT::Logger->error("The WebBaseURL config option must contain a scheme (http or https)");
824             }
825
826             if ($value =~ m{/$}) {
827                 $RT::Logger->error("The WebBaseURL config option requires no trailing slash");
828             }
829
830             if ($value =~ m{^https?://.+?(/[^/].*)}i) {
831                 $RT::Logger->error("The WebBaseURL config option must not contain a path ($1)");
832             }
833         },
834     },
835     WebURL => {
836         PostLoadCheck => sub {
837             my $self  = shift;
838             my $value = shift;
839
840             if (!$value) {
841                 $RT::Logger->error("You must set the WebURL config option");
842                 return;
843             }
844
845             if ($value !~ m{^https?://}i) {
846                 $RT::Logger->error("The WebURL config option must contain a scheme (http or https)");
847             }
848
849             if ($value !~ m{/$}) {
850                 $RT::Logger->error("The WebURL config option requires a trailing slash");
851             }
852         },
853     },
854     EmailInputEncodings => {
855         Type => 'ARRAY',
856         PostLoadCheck => sub {
857             my $self  = shift;
858             my $value = $self->Get('EmailInputEncodings');
859             return unless $value && @$value;
860
861             my %seen;
862             foreach my $encoding ( grep defined && length, splice @$value ) {
863                 next if $seen{ $encoding };
864                 if ( $encoding eq '*' ) {
865                     unshift @$value, '*';
866                     next;
867                 }
868
869                 my $canonic = Encode::resolve_alias( $encoding );
870                 unless ( $canonic ) {
871                     warn "Unknown encoding '$encoding' in \@EmailInputEncodings option";
872                 }
873                 elsif ( $seen{ $canonic }++ ) {
874                     next;
875                 }
876                 else {
877                     push @$value, $canonic;
878                 }
879             }
880         },
881     },
882     LogToScreen => {
883         Deprecated => {
884             Instead => 'LogToSTDERR',
885             Remove  => '4.4',
886         },
887     },
888     UserAutocompleteFields => {
889         Deprecated => {
890             Instead => 'UserSearchFields',
891             Remove  => '4.4',
892         },
893     },
894     CustomFieldGroupings => {
895         Type            => 'HASH',
896         PostLoadCheck   => sub {
897             my $config = shift;
898             # use scalar context intentionally to avoid not a hash error
899             my $groups = $config->Get('CustomFieldGroupings') || {};
900
901             unless (ref($groups) eq 'HASH') {
902                 RT->Logger->error("Config option \%CustomFieldGroupings is a @{[ref $groups]} not a HASH; ignoring");
903                 $groups = {};
904             }
905
906             for my $class (keys %$groups) {
907                 my @h;
908                 if (ref($groups->{$class}) eq 'HASH') {
909                     push @h, $_, $groups->{$class}->{$_}
910                         for sort {lc($a) cmp lc($b)} keys %{ $groups->{$class} };
911                 } elsif (ref($groups->{$class}) eq 'ARRAY') {
912                     @h = @{ $groups->{$class} };
913                 } else {
914                     RT->Logger->error("Config option \%CustomFieldGroupings{$class} is not a HASH or ARRAY; ignoring");
915                     delete $groups->{$class};
916                     next;
917                 }
918
919                 $groups->{$class} = [];
920                 while (@h) {
921                     my $group = shift @h;
922                     my $ref   = shift @h;
923                     if (ref($ref) eq 'ARRAY') {
924                         push @{$groups->{$class}}, $group => $ref;
925                     } else {
926                         RT->Logger->error("Config option \%CustomFieldGroupings{$class}{$group} is not an ARRAY; ignoring");
927                     }
928                 }
929             }
930             $config->Set( CustomFieldGroupings => %$groups );
931         },
932     },
933     ChartColors => {
934         Type    => 'ARRAY',
935     },
936     WebExternalAuth           => { Deprecated => { Instead => 'WebRemoteUserAuth',             Remove => '4.4' }},
937     WebExternalAuthContinuous => { Deprecated => { Instead => 'WebRemoteUserContinuous',       Remove => '4.4' }},
938     WebFallbackToInternalAuth => { Deprecated => { Instead => 'WebFallbackToRTLogin',          Remove => '4.4' }},
939     WebExternalGecos          => { Deprecated => { Instead => 'WebRemoteUserGecos',            Remove => '4.4' }},
940     WebExternalAuto           => { Deprecated => { Instead => 'WebRemoteUserAutocreate',       Remove => '4.4' }},
941     AutoCreate                => { Deprecated => { Instead => 'UserAutocreateDefaultsOnLogin', Remove => '4.4' }},
942 );
943 my %OPTIONS = ();
944 my @LOADED_CONFIGS = ();
945
946 =head1 METHODS
947
948 =head2 new
949
950 Object constructor returns new object. Takes no arguments.
951
952 =cut
953
954 sub new {
955     my $proto = shift;
956     my $class = ref($proto) ? ref($proto) : $proto;
957     my $self  = bless {}, $class;
958     $self->_Init(@_);
959     return $self;
960 }
961
962 sub _Init {
963     return;
964 }
965
966 =head2 LoadConfigs
967
968 Load all configs. First of all load RT's config then load
969 extensions' config files in alphabetical order.
970 Takes no arguments.
971
972 =cut
973
974 sub LoadConfigs {
975     my $self    = shift;
976
977     $self->LoadConfig( File => 'RT_Config.pm' );
978
979     my @configs = $self->Configs;
980     $self->LoadConfig( File => $_ ) foreach @configs;
981     return;
982 }
983
984 =head1 LoadConfig
985
986 Takes param hash with C<File> field.
987 First, the site configuration file is loaded, in order to establish
988 overall site settings like hostname and name of RT instance.
989 Then, the core configuration file is loaded to set fallback values
990 for all settings; it bases some values on settings from the site
991 configuration file.
992
993 B<Note> that core config file don't change options if site config
994 has set them so to add value to some option instead of
995 overriding you have to copy original value from core config file.
996
997 =cut
998
999 sub LoadConfig {
1000     my $self = shift;
1001     my %args = ( File => '', @_ );
1002     $args{'File'} =~ s/(?<!Site)(?=Config\.pm$)/Site/;
1003     if ( $args{'File'} eq 'RT_SiteConfig.pm'
1004         and my $site_config = $ENV{RT_SITE_CONFIG} )
1005     {
1006         $self->_LoadConfig( %args, File => $site_config );
1007         # to allow load siteconfig again and again in case it's updated
1008         delete $INC{ $site_config };
1009     } else {
1010         $self->_LoadConfig(%args);
1011         delete $INC{$args{'File'}};
1012     }
1013
1014     $args{'File'} =~ s/Site(?=Config\.pm$)//;
1015     $self->_LoadConfig(%args);
1016     return 1;
1017 }
1018
1019 sub _LoadConfig {
1020     my $self = shift;
1021     my %args = ( File => '', @_ );
1022
1023     my ($is_ext, $is_site);
1024     if ( $args{'File'} eq ($ENV{RT_SITE_CONFIG}||'') ) {
1025         ($is_ext, $is_site) = ('', 1);
1026     } else {
1027         $is_ext = $args{'File'} =~ /^(?!RT_)(?:(.*)_)(?:Site)?Config/ ? $1 : '';
1028         $is_site = $args{'File'} =~ /SiteConfig/ ? 1 : 0;
1029     }
1030
1031     eval {
1032         package RT;
1033         local *Set = sub(\[$@%]@) {
1034             my ( $opt_ref, @args ) = @_;
1035             my ( $pack, $file, $line ) = caller;
1036             return $self->SetFromConfig(
1037                 Option     => $opt_ref,
1038                 Value      => [@args],
1039                 Package    => $pack,
1040                 File       => $file,
1041                 Line       => $line,
1042                 SiteConfig => $is_site,
1043                 Extension  => $is_ext,
1044             );
1045         };
1046         local *Plugin = sub {
1047             my (@new_plugins) = @_;
1048             my ( $pack, $file, $line ) = caller;
1049             return $self->SetFromConfig(
1050                 Option     => \@RT::Plugins,
1051                 Value      => [@RT::Plugins, @new_plugins],
1052                 Package    => $pack,
1053                 File       => $file,
1054                 Line       => $line,
1055                 SiteConfig => $is_site,
1056                 Extension  => $is_ext,
1057             );
1058         };
1059         my @etc_dirs = ($RT::LocalEtcPath);
1060         push @etc_dirs, RT->PluginDirs('etc') if $is_ext;
1061         push @etc_dirs, $RT::EtcPath, @INC;
1062         local @INC = @etc_dirs;
1063         require $args{'File'};
1064     };
1065     if ($@) {
1066         return 1 if $is_site && $@ =~ /^Can't locate \Q$args{File}/;
1067         if ( $is_site || $@ !~ /^Can't locate \Q$args{File}/ ) {
1068             die qq{Couldn't load RT config file $args{'File'}:\n\n$@};
1069         }
1070
1071         my $username = getpwuid($>);
1072         my $group    = getgrgid($();
1073
1074         my ( $file_path, $fileuid, $filegid );
1075         foreach ( $RT::LocalEtcPath, $RT::EtcPath, @INC ) {
1076             my $tmp = File::Spec->catfile( $_, $args{File} );
1077             ( $fileuid, $filegid ) = ( stat($tmp) )[ 4, 5 ];
1078             if ( defined $fileuid ) {
1079                 $file_path = $tmp;
1080                 last;
1081             }
1082         }
1083         unless ($file_path) {
1084             die
1085                 qq{Couldn't load RT config file $args{'File'} as user $username / group $group.\n}
1086                 . qq{The file couldn't be found in $RT::LocalEtcPath and $RT::EtcPath.\n$@};
1087         }
1088
1089         my $message = <<EOF;
1090
1091 RT couldn't load RT config file %s as:
1092     user: $username 
1093     group: $group
1094
1095 The file is owned by user %s and group %s.  
1096
1097 This usually means that the user/group your webserver is running
1098 as cannot read the file.  Be careful not to make the permissions
1099 on this file too liberal, because it contains database passwords.
1100 You may need to put the webserver user in the appropriate group
1101 (%s) or change permissions be able to run succesfully.
1102 EOF
1103
1104         my $fileusername = getpwuid($fileuid);
1105         my $filegroup    = getgrgid($filegid);
1106         my $errormessage = sprintf( $message,
1107             $file_path, $fileusername, $filegroup, $filegroup );
1108         die "$errormessage\n$@";
1109     } else {
1110         # Loaded successfully
1111         push @LOADED_CONFIGS, {
1112             as          => $args{'File'},
1113             filename    => $INC{ $args{'File'} },
1114             extension   => $is_ext,
1115             site        => $is_site,
1116         };
1117     }
1118     return 1;
1119 }
1120
1121 sub PostLoadCheck {
1122     my $self = shift;
1123     foreach my $o ( grep $META{$_}{'PostLoadCheck'}, $self->Options( Overridable => undef ) ) {
1124         $META{$o}->{'PostLoadCheck'}->( $self, $self->Get($o) );
1125     }
1126 }
1127
1128 =head2 Configs
1129
1130 Returns list of config files found in local etc, plugins' etc
1131 and main etc directories.
1132
1133 =cut
1134
1135 sub Configs {
1136     my $self    = shift;
1137
1138     my @configs = ();
1139     foreach my $path ( $RT::LocalEtcPath, RT->PluginDirs('etc'), $RT::EtcPath ) {
1140         my $mask = File::Spec->catfile( $path, "*_Config.pm" );
1141         my @files = glob $mask;
1142         @files = grep !/^RT_Config\.pm$/,
1143             grep $_ && /^\w+_Config\.pm$/,
1144             map { s/^.*[\\\/]//; $_ } @files;
1145         push @configs, sort @files;
1146     }
1147
1148     my %seen;
1149     @configs = grep !$seen{$_}++, @configs;
1150     return @configs;
1151 }
1152
1153 =head2 LoadedConfigs
1154
1155 Returns a list of hashrefs, one for each config file loaded.  The keys of the
1156 hashes are:
1157
1158 =over 4
1159
1160 =item as
1161
1162 Name this config file was loaded as (relative filename usually).
1163
1164 =item filename
1165
1166 The full path and filename.
1167
1168 =item extension
1169
1170 The "extension" part of the filename.  For example, the file C<RTIR_Config.pm>
1171 will have an C<extension> value of C<RTIR>.
1172
1173 =item site
1174
1175 True if the file is considered a site-level override.  For example, C<site>
1176 will be false for C<RT_Config.pm> and true for C<RT_SiteConfig.pm>.
1177
1178 =back
1179
1180 =cut
1181
1182 sub LoadedConfigs {
1183     # Copy to avoid the caller changing our internal data
1184     return map { \%$_ } @LOADED_CONFIGS
1185 }
1186
1187 =head2 Get
1188
1189 Takes name of the option as argument and returns its current value.
1190
1191 In the case of a user-overridable option, first checks the user's
1192 preferences before looking for site-wide configuration.
1193
1194 Returns values from RT_SiteConfig, RT_Config and then the %META hash
1195 of configuration variables's "Default" for this config variable,
1196 in that order.
1197
1198 Returns different things in scalar and array contexts. For scalar
1199 options it's not that important, however for arrays and hash it's.
1200 In scalar context returns references to arrays and hashes.
1201
1202 Use C<scalar> perl's op to force context, especially when you use
1203 C<(..., Argument => RT->Config->Get('ArrayOpt'), ...)>
1204 as perl's '=>' op doesn't change context of the right hand argument to
1205 scalar. Instead use C<(..., Argument => scalar RT->Config->Get('ArrayOpt'), ...)>.
1206
1207 It's also important for options that have no default value(no default
1208 in F<etc/RT_Config.pm>). If you don't force scalar context then you'll
1209 get empty list and all your named args will be messed up. For example
1210 C<(arg1 => 1, arg2 => RT->Config->Get('OptionDoesNotExist'), arg3 => 3)>
1211 will result in C<(arg1 => 1, arg2 => 'arg3', 3)> what is most probably
1212 unexpected, or C<(arg1 => 1, arg2 => RT->Config->Get('ArrayOption'), arg3 => 3)>
1213 will result in C<(arg1 => 1, arg2 => 'element of option', 'another_one' => ..., 'arg3', 3)>.
1214
1215 =cut
1216
1217 sub Get {
1218     my ( $self, $name, $user ) = @_;
1219
1220     my $res;
1221     if ( $user && $user->id && $META{$name}->{'Overridable'} ) {
1222         $user = $user->UserObj if $user->isa('RT::CurrentUser');
1223         my $prefs = $user->Preferences($RT::System);
1224         $res = $prefs->{$name} if $prefs;
1225     }
1226     $res = $OPTIONS{$name}           unless defined $res;
1227     $res = $META{$name}->{'Default'} unless defined $res;
1228     return $self->_ReturnValue( $res, $META{$name}->{'Type'} || 'SCALAR' );
1229 }
1230
1231 =head2 GetObfuscated
1232
1233 the same as Get, except it returns Obfuscated value via Obfuscate sub
1234
1235 =cut
1236
1237 sub GetObfuscated {
1238     my $self = shift;
1239     my ( $name, $user ) = @_;
1240     my $obfuscate = $META{$name}->{Obfuscate};
1241
1242     # we use two Get here is to simplify the logic of the return value
1243     # configs need obfuscation are supposed to be less, so won't be too heavy
1244
1245     return $self->Get(@_) unless $obfuscate;
1246
1247     my $res = $self->Get(@_);
1248     $res = $obfuscate->( $self, $res, $user );
1249     return $self->_ReturnValue( $res, $META{$name}->{'Type'} || 'SCALAR' );
1250 }
1251
1252 =head2 Set
1253
1254 Set option's value to new value. Takes name of the option and new value.
1255 Returns old value.
1256
1257 The new value should be scalar, array or hash depending on type of the option.
1258 If the option is not defined in meta or the default RT config then it is of
1259 scalar type.
1260
1261 =cut
1262
1263 sub Set {
1264     my ( $self, $name ) = ( shift, shift );
1265
1266     my $old = $OPTIONS{$name};
1267     my $type = $META{$name}->{'Type'} || 'SCALAR';
1268     if ( $type eq 'ARRAY' ) {
1269         $OPTIONS{$name} = [@_];
1270         { no warnings 'once'; no strict 'refs'; @{"RT::$name"} = (@_); }
1271     } elsif ( $type eq 'HASH' ) {
1272         $OPTIONS{$name} = {@_};
1273         { no warnings 'once'; no strict 'refs'; %{"RT::$name"} = (@_); }
1274     } else {
1275         $OPTIONS{$name} = shift;
1276         {no warnings 'once'; no strict 'refs'; ${"RT::$name"} = $OPTIONS{$name}; }
1277     }
1278     $META{$name}->{'Type'} = $type;
1279     $META{$name}->{'PostSet'}->($self, $OPTIONS{$name}, $old)
1280         if $META{$name}->{'PostSet'};
1281     if ($META{$name}->{'Deprecated'}) {
1282         my %deprecated = %{$META{$name}->{'Deprecated'}};
1283         my $new_var = $deprecated{Instead} || '';
1284         $self->SetFromConfig(
1285             Option => \$new_var,
1286             Value  => [$OPTIONS{$name}],
1287             %{$self->Meta($name)->{'Source'}}
1288         ) if $new_var;
1289         $META{$name}->{'PostLoadCheck'} ||= sub {
1290             RT->Deprecated(
1291                 Message => "Configuration option $name is deprecated",
1292                 Stack   => 0,
1293                 %deprecated,
1294             );
1295         };
1296     }
1297     return $self->_ReturnValue( $old, $type );
1298 }
1299
1300 sub _ReturnValue {
1301     my ( $self, $res, $type ) = @_;
1302     return $res unless wantarray;
1303
1304     if ( $type eq 'ARRAY' ) {
1305         return @{ $res || [] };
1306     } elsif ( $type eq 'HASH' ) {
1307         return %{ $res || {} };
1308     }
1309     return $res;
1310 }
1311
1312 sub SetFromConfig {
1313     my $self = shift;
1314     my %args = (
1315         Option     => undef,
1316         Value      => [],
1317         Package    => 'RT',
1318         File       => '',
1319         Line       => 0,
1320         SiteConfig => 1,
1321         Extension  => 0,
1322         @_
1323     );
1324
1325     unless ( $args{'File'} ) {
1326         ( $args{'Package'}, $args{'File'}, $args{'Line'} ) = caller(1);
1327     }
1328
1329     my $opt = $args{'Option'};
1330
1331     my $type;
1332     my $name = Symbol::Global::Name->find($opt);
1333     if ($name) {
1334         $type = ref $opt;
1335         $name =~ s/.*:://;
1336     } else {
1337         $name = $$opt;
1338         $type = $META{$name}->{'Type'} || 'SCALAR';
1339     }
1340
1341     # if option is already set we have to check where
1342     # it comes from and may be ignore it
1343     if ( exists $OPTIONS{$name} ) {
1344         if ( $type eq 'HASH' ) {
1345             $args{'Value'} = [
1346                 @{ $args{'Value'} },
1347                 @{ $args{'Value'} }%2? (undef) : (),
1348                 $self->Get( $name ),
1349             ];
1350         } elsif ( $args{'SiteConfig'} && $args{'Extension'} ) {
1351             # if it's site config of an extension then it can only
1352             # override options that came from its main config
1353             if ( $args{'Extension'} ne $META{$name}->{'Source'}{'Extension'} ) {
1354                 my %source = %{ $META{$name}->{'Source'} };
1355                 warn
1356                     "Change of config option '$name' at $args{'File'} line $args{'Line'} has been ignored."
1357                     ." This option earlier has been set in $source{'File'} line $source{'Line'}."
1358                     ." To overide this option use ". ($source{'Extension'}||'RT')
1359                     ." site config."
1360                 ;
1361                 return 1;
1362             }
1363         } elsif ( !$args{'SiteConfig'} && $META{$name}->{'Source'}{'SiteConfig'} ) {
1364             # if it's core config then we can override any option that came from another
1365             # core config, but not site config
1366
1367             my %source = %{ $META{$name}->{'Source'} };
1368             if ( $source{'Extension'} ne $args{'Extension'} ) {
1369                 # as a site config is loaded earlier then its base config
1370                 # then we warn only on different extensions, for example
1371                 # RTIR's options is set in main site config
1372                 warn
1373                     "Change of config option '$name' at $args{'File'} line $args{'Line'} has been ignored."
1374                     ." It may be ok, but we want you to be aware."
1375                     ." This option has been set earlier in $source{'File'} line $source{'Line'}."
1376                 ;
1377             }
1378
1379             return 1;
1380         }
1381     }
1382
1383     $META{$name}->{'Type'} = $type;
1384     foreach (qw(Package File Line SiteConfig Extension)) {
1385         $META{$name}->{'Source'}->{$_} = $args{$_};
1386     }
1387     $self->Set( $name, @{ $args{'Value'} } );
1388
1389     return 1;
1390 }
1391
1392 =head2 Metadata
1393
1394
1395 =head2 Meta
1396
1397 =cut
1398
1399 sub Meta {
1400     return $META{ $_[1] };
1401 }
1402
1403 sub Sections {
1404     my $self = shift;
1405     my %seen;
1406     my @sections = sort
1407         grep !$seen{$_}++,
1408         map $_->{'Section'} || 'General',
1409         values %META;
1410     return @sections;
1411 }
1412
1413 sub Options {
1414     my $self = shift;
1415     my %args = ( Section => undef, Overridable => 1, Sorted => 1, @_ );
1416     my @res  = sort keys %META;
1417     
1418     @res = grep( ( $META{$_}->{'Section'} || 'General' ) eq $args{'Section'},
1419         @res 
1420     ) if defined $args{'Section'};
1421
1422     if ( defined $args{'Overridable'} ) {
1423         @res
1424             = grep( ( $META{$_}->{'Overridable'} || 0 ) == $args{'Overridable'},
1425             @res );
1426     }
1427
1428     if ( $args{'Sorted'} ) {
1429         @res = sort {
1430             ($META{$a}->{SortOrder}||9999) <=> ($META{$b}->{SortOrder}||9999)
1431             || $a cmp $b 
1432         } @res;
1433     } else {
1434         @res = sort { $a cmp $b } @res;
1435     }
1436     return @res;
1437 }
1438
1439 =head2 AddOption( Name => '', Section => '', ... )
1440
1441 =cut
1442
1443 sub AddOption {
1444     my $self = shift;
1445     my %args = (
1446         Name            => undef,
1447         Section         => undef,
1448         Overridable     => 0,
1449         SortOrder       => undef,
1450         Widget          => '/Widgets/Form/String',
1451         WidgetArguments => {},
1452         @_
1453     );
1454
1455     unless ( $args{Name} ) {
1456         $RT::Logger->error("Need Name to add a new config");
1457         return;
1458     }
1459
1460     unless ( $args{Section} ) {
1461         $RT::Logger->error("Need Section to add a new config option");
1462         return;
1463     }
1464
1465     $META{ delete $args{Name} } = \%args;
1466 }
1467
1468 =head2 DeleteOption( Name => '' )
1469
1470 =cut
1471
1472 sub DeleteOption {
1473     my $self = shift;
1474     my %args = (
1475         Name            => undef,
1476         @_
1477         );
1478     if ( $args{Name} ) {
1479         delete $META{$args{Name}};
1480     }
1481     else {
1482         $RT::Logger->error("Need Name to remove a config option");
1483         return;
1484     }
1485 }
1486
1487 =head2 UpdateOption( Name => '' ), Section => '', ... )
1488
1489 =cut
1490
1491 sub UpdateOption {
1492     my $self = shift;
1493     my %args = (
1494         Name            => undef,
1495         Section         => undef,
1496         Overridable     => undef,
1497         SortOrder       => undef,
1498         Widget          => undef,
1499         WidgetArguments => undef,
1500         @_
1501     );
1502
1503     my $name = delete $args{Name};
1504
1505     unless ( $name ) {
1506         $RT::Logger->error("Need Name to update a new config");
1507         return;
1508     }
1509
1510     unless ( exists $META{$name} ) {
1511         $RT::Logger->error("Config $name doesn't exist");
1512         return;
1513     }
1514
1515     for my $type ( keys %args ) {
1516         next unless defined $args{$type};
1517         $META{$name}{$type} = $args{$type};
1518     }
1519     return 1;
1520 }
1521
1522 RT::Base->_ImportOverlays();
1523
1524 1;