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