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