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