1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
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
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.
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.
30 # CONTRIBUTION SUBMISSION POLICY:
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.)
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.
47 # END BPS TAGGED BLOCK }}}
56 use Symbol::Global::Name;
57 use List::MoreUtils 'uniq';
61 RT::Config - RT's config
67 my $config = RT::Config->new;
71 my $rt_web_path = $config->Get('WebPath');
72 $config->Set(EmailOutputEncoding => 'latin1');
74 # get config object from RT package
77 my $config = RT->Config;
81 C<RT::Config> class provide access to RT's and RT extensions' config files.
83 RT uses two files for site configuring:
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.>
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.
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.
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
105 Hash of Config options that may be user overridable
106 or may require more logic than should live in RT_*Config.pm
108 Keyed by config name, there are several properties that
109 can be set for each config optin:
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
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()
145 # General user overridable options
147 Section => 'General',
150 Widget => '/Widgets/Form/Select',
152 Description => 'Default queue', #loc
154 my $ret = { Values => [], ValuesLabel => {}};
155 my $q = RT::Queues->new($HTML::Mason::Commands::session{'CurrentUser'});
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;
166 RememberDefaultQueue => {
167 Section => 'General',
170 Widget => '/Widgets/Form/Boolean',
172 Description => 'Remember default queue' # loc
176 Section => 'General',
179 Widget => '/Widgets/Form/Select',
181 Description => 'Username format', # loc
182 Values => [qw(role concise verbose)],
184 role => 'Privileged: usernames; Unprivileged: names and email addresses', # loc
185 concise => 'Short usernames', # loc
186 verbose => 'Name and email address', # loc
190 AutocompleteOwners => {
191 Section => 'General',
194 Widget => '/Widgets/Form/Boolean',
196 Description => 'Use autocomplete to find owners?', # loc
197 Hints => 'Replaces the owner dropdowns with textboxes' #loc
200 WebDefaultStylesheet => {
201 Section => 'General', #loc
204 Widget => '/Widgets/Form/Select',
206 Description => 'Theme', #loc
209 unless (@stylesheets) {
210 for my $static_path ( RT::Interface::Web->StaticRoots ) {
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' )
220 RT->Logger->error("Can't read $css_path: $!");
223 @stylesheets = sort { lc $a cmp lc $b } uniq @stylesheets;
225 return { Values => \@stylesheets };
228 PostLoadCheck => sub {
230 my $value = $self->Get('WebDefaultStylesheet');
232 my @roots = RT::Interface::Web->StaticRoots;
233 for my $root (@roots) {
234 return if -d "$root/css/$value";
237 $RT::Logger->warning(
238 "The default stylesheet ($value) does not exist in this instance of RT. "
239 . "Defaulting to freeside4."
242 $self->Set('WebDefaultStylesheet', 'freeside4');
246 Section => 'General',
249 Widget => '/Widgets/Form/Boolean',
251 Description => 'Include time in iCal feed events?', # loc
252 Hints => 'Formats iCal feed events with date and time' #loc
255 UseSideBySideLayout => {
256 Section => 'Ticket composition',
259 Widget => '/Widgets/Form/Boolean',
261 Description => 'Use a two column layout for create and update forms?' # loc
264 MessageBoxRichText => {
265 Section => 'Ticket composition',
268 Widget => '/Widgets/Form/Boolean',
270 Description => 'WYSIWYG message composer' # loc
273 MessageBoxRichTextHeight => {
274 Section => 'Ticket composition',
277 Widget => '/Widgets/Form/Integer',
279 Description => 'WYSIWYG composer height', # loc
283 Section => 'Ticket composition',
286 Widget => '/Widgets/Form/Integer',
288 Description => 'Message box width', #loc
291 MessageBoxHeight => {
292 Section => 'Ticket composition',
295 Widget => '/Widgets/Form/Integer',
297 Description => 'Message box height', #loc
300 DefaultTimeUnitsToHours => {
301 Section => 'Ticket composition', #loc
304 Widget => '/Widgets/Form/Boolean',
306 Description => 'Enter time in hours by default', #loc
307 Hints => 'Only for entry, not display', #loc
310 SearchResultsRefreshInterval => {
311 Section => 'General', #loc
314 Widget => '/Widgets/Form/Select',
316 Description => 'Search results refresh interval', #loc
317 Values => [qw(0 120 300 600 1200 3600 7200)],
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
330 # User overridable options for RT at a glance
331 HomePageRefreshInterval => {
332 Section => 'RT at a glance', #loc
335 Widget => '/Widgets/Form/Select',
337 Description => 'Home page refresh interval', #loc
338 Values => [qw(0 120 300 600 1200 3600 7200)],
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
351 # User overridable options for Ticket displays
353 Section => 'Ticket display', # loc
356 Widget => '/Widgets/Form/Boolean',
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
363 Section => 'Ticket display', #loc
366 Widget => '/Widgets/Form/Integer',
368 Description => 'Maximum inline message length', #loc
370 "Length in characters; Use '0' to show all messages inline, regardless of length" #loc
373 OldestTransactionsFirst => {
374 Section => 'Ticket display',
377 Widget => '/Widgets/Form/Boolean',
379 Description => 'Show oldest history first', #loc
383 Section => 'Ticket display',
386 Widget => '/Widgets/Form/Select',
388 Description => 'Show history', #loc
389 Values => [qw(delay click always)],
391 delay => "after the rest of the page loads", #loc
392 click => "after clicking a link", #loc
393 always => "immediately", #loc
397 ShowUnreadMessageNotifications => {
398 Section => 'Ticket display',
401 Widget => '/Widgets/Form/Boolean',
403 Description => 'Notify me of unread messages', #loc
411 $self->SetFromConfig(
412 Option => \'PlainTextMono',
414 %{$self->Meta('PlainTextPre')->{'Source'}}
417 PostLoadCheck => sub {
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'};
425 Section => 'Ticket display',
428 Widget => '/Widgets/Form/Boolean',
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
434 MoreAboutRequestorTicketList => {
435 Section => 'Ticket display', #loc
438 Widget => '/Widgets/Form/Select',
440 Description => 'What tickets to display in the "More about requestor" box', #loc
441 Values => [qw(Active Inactive All None)],
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
450 SimplifiedRecipients => {
451 Section => 'Ticket display', #loc
454 Widget => '/Widgets/Form/Boolean',
456 Description => "Show simplified recipient list on ticket update", #loc
459 DisplayTicketAfterQuickCreate => {
460 Section => 'Ticket display',
463 Widget => '/Widgets/Form/Boolean',
465 Description => 'Display ticket after "Quick Create"', #loc
469 Section => 'Ticket display',
472 Widget => '/Widgets/Form/Boolean',
474 Description => 'Enable quote folding?' # loc
478 # User overridable locale options
480 Section => 'Locale', #loc
482 Widget => '/Widgets/Form/Select',
484 Description => 'Date format', #loc
485 Callback => sub { my $ret = { Values => [], ValuesLabel => {}};
486 my $date = RT::Date->new($HTML::Mason::Commands::session{'CurrentUser'});
488 foreach my $value ($date->Formatters) {
489 push @{$ret->{Values}}, $value;
490 $ret->{ValuesLabel}{$value} = $date->Get(
502 PostLoadCheck => sub {
504 my $value = $self->Get('RTAddressRegexp');
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.'
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/;
525 # User overridable mail options
527 Section => 'Mail', #loc
529 Default => 'Individual messages',
530 Widget => '/Widgets/Form/Select',
532 Description => 'Email delivery', #loc
534 'Individual messages', #loc
536 'Weekly digest', #loc
542 Section => 'Mail', #loc
545 Widget => '/Widgets/Form/Boolean',
547 Description => 'Outgoing mail', #loc
548 Hints => 'Should RT send you mail for ticket updates you make?', #loc
552 # this tends to break extensions that stash links in ticket update pages
555 PostLoadCheck => sub {
556 my ($self,$value) = @_;
557 $RT::Logger->error("your \$Organization setting ($value) appears to contain whitespace. Please fix this.")
562 # Internal config options
563 DatabaseExtraDSN => {
569 PostLoadCheck => sub {
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;
579 } elsif ($dbtype eq 'Pg') {
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;
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;
597 my (undef, $create) = eval { $RT::Handle->dbh->selectrow_array("SHOW CREATE TABLE " . $v->{Table}); };
598 my ($engine) = ($create||'') =~ /engine=(\S+)/i;
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
605 unless ($v->{'MaxMatches'}) {
606 $RT::Logger->warn("No MaxMatches set for full-text index; defaulting to 10000");
607 $v->{MaxMatches} = 10_000;
610 # Internal, one-column table
611 $v->{Column} = 'Content';
612 $v->{Engine} = $engine;
616 $RT::Logger->error("Indexed full-text-search not supported for $dbtype");
623 PostLoadCheck => sub {
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 );
634 PostLoadCheck => sub {
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 );
645 PostLoadCheck => sub {
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' );
656 PostLoadCheck => sub { RT::Interface::Email->_HTMLFormatter },
660 PostLoadCheck => sub {
663 # Make sure Crypt is post-loaded first
664 $META{Crypt}{'PostLoadCheck'}->( $self, $self->Get( 'Crypt' ) );
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.'
678 $_ eq 'Auth::GnuPG' || $_ eq 'Auth::SMIME'
681 $self->Set( MailPlugins => @plugins );
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");
691 PostLoadCheck => sub {
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 ) );
708 my $opt = $self->Get('Crypt');
709 my @enabled = RT::Crypt->EnabledProtocols;
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'}} ];
720 $opt->{'Incoming'} = \@enabled;
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"));
728 $opt->{'Outgoing'} = $enabled[0] unless $enabled{$opt->{'Outgoing'}};
730 $opt->{'Outgoing'} = $enabled[0];
736 PostLoadCheck => sub {
738 my $opt = $self->Get('SMIME');
739 return unless $opt->{'Enable'};
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} );
745 unless (-d $opt->{Keyring} and -r _) {
747 "RT's SMIME libraries couldn't successfully read your".
748 " configured SMIME keyring directory (".$opt->{Keyring}
750 delete $opt->{Keyring};
754 if (defined $opt->{CAPath}) {
755 if (-d $opt->{CAPath} and -r _) {
757 } elsif (-f $opt->{CAPath} and -r _) {
761 "RT's SMIME libraries could not read your configured CAPath (".$opt->{CAPath}.")"
763 delete $opt->{CAPath};
770 PostLoadCheck => sub {
772 my $gpg = $self->Get('GnuPG');
773 return unless $gpg->{'Enable'};
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} );
779 unless (-d $gpgopts->{homedir} && -r _ ) { # no homedir, no gpg
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;
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."
794 delete $gpg->{$_} for qw(RejectOnMissingPrivateKey RejectOnBadData AllowEncryptDataInDB);
798 GnuPGOptions => { Type => 'HASH' },
799 ReferrerWhitelist => { Type => 'ARRAY' },
801 PostLoadCheck => sub {
805 # "In most cases, you should leave $WebPath set to '' (an empty value)."
806 return unless $value;
808 # try to catch someone who assumes that you shouldn't leave this empty
810 $RT::Logger->error("For the WebPath config option, use the empty string instead of /");
814 # $WebPath requires a leading / but no trailing /, or it can be blank.
815 return if $value =~ m{^/.+[^/]$};
817 if ($value =~ m{/$}) {
818 $RT::Logger->error("The WebPath config option requires no trailing slash");
821 if ($value !~ m{^/}) {
822 $RT::Logger->error("The WebPath config option requires a leading slash");
827 PostLoadCheck => sub {
832 $RT::Logger->error("You must set the WebDomain config option");
836 if ($value =~ m{^(\w+://)}) {
837 $RT::Logger->error("The WebDomain config option must not contain a scheme ($1)");
841 if ($value =~ m{(/.*)}) {
842 $RT::Logger->error("The WebDomain config option must not contain a path ($1)");
846 if ($value =~ m{:(\d*)}) {
847 $RT::Logger->error("The WebDomain config option must not contain a port ($1)");
853 PostLoadCheck => sub {
858 $RT::Logger->error("You must set the WebPort config option");
862 if ($value !~ m{^\d+$}) {
863 $RT::Logger->error("The WebPort config option must be an integer");
868 PostLoadCheck => sub {
873 $RT::Logger->error("You must set the WebBaseURL config option");
877 if ($value !~ m{^https?://}i) {
878 $RT::Logger->error("The WebBaseURL config option must contain a scheme (http or https)");
881 if ($value =~ m{/$}) {
882 $RT::Logger->error("The WebBaseURL config option requires no trailing slash");
885 if ($value =~ m{^https?://.+?(/[^/].*)}i) {
886 $RT::Logger->error("The WebBaseURL config option must not contain a path ($1)");
891 PostLoadCheck => sub {
896 $RT::Logger->error("You must set the WebURL config option");
900 if ($value !~ m{^https?://}i) {
901 $RT::Logger->error("The WebURL config option must contain a scheme (http or https)");
904 if ($value !~ m{/$}) {
905 $RT::Logger->error("The WebURL config option requires a trailing slash");
909 EmailInputEncodings => {
911 PostLoadCheck => sub {
913 my $value = $self->Get('EmailInputEncodings');
914 return unless $value && @$value;
917 foreach my $encoding ( grep defined && length, splice @$value ) {
918 next if $seen{ $encoding };
919 if ( $encoding eq '*' ) {
920 unshift @$value, '*';
924 my $canonic = Encode::resolve_alias( $encoding );
925 unless ( $canonic ) {
926 warn "Unknown encoding '$encoding' in \@EmailInputEncodings option";
928 elsif ( $seen{ $canonic }++ ) {
932 push @$value, $canonic;
939 Instead => 'LogToSTDERR',
943 UserAutocompleteFields => {
945 Instead => 'UserSearchFields',
949 CustomFieldGroupings => {
951 PostLoadCheck => sub {
953 # use scalar context intentionally to avoid not a hash error
954 my $groups = $config->Get('CustomFieldGroupings') || {};
956 unless (ref($groups) eq 'HASH') {
957 RT->Logger->error("Config option \%CustomFieldGroupings is a @{[ref $groups]} not a HASH; ignoring");
961 for my $class (keys %$groups) {
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} };
969 RT->Logger->error("Config option \%CustomFieldGroupings{$class} is not a HASH or ARRAY; ignoring");
970 delete $groups->{$class};
974 $groups->{$class} = [];
976 my $group = shift @h;
978 if (ref($ref) eq 'ARRAY') {
979 push @{$groups->{$class}}, $group => $ref;
981 RT->Logger->error("Config option \%CustomFieldGroupings{$class}{$group} is not an ARRAY; ignoring");
985 $config->Set( CustomFieldGroupings => %$groups );
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' }},
1000 Message => "The LogoImageHeight configuration option did not affect display, and has been removed; please remove it from your RT_SiteConfig.pm",
1006 Message => "The LogoImageWidth configuration option did not affect display, and has been removed; please remove it from your RT_SiteConfig.pm",
1009 DatabaseRequireSSL => {
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.",
1018 my @LOADED_CONFIGS = ();
1024 Object constructor returns new object. Takes no arguments.
1030 my $class = ref($proto) ? ref($proto) : $proto;
1031 my $self = bless {}, $class;
1042 Load all configs. First of all load RT's config then load
1043 extensions' config files in alphabetical order.
1051 $self->LoadConfig( File => 'RT_Config.pm' );
1053 my @configs = $self->Configs;
1054 $self->LoadConfig( File => $_ ) foreach @configs;
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
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.
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} )
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 };
1084 $self->_LoadConfig(%args);
1085 delete $INC{$args{'File'}};
1088 $args{'File'} =~ s/Site(?=Config\.pm$)//;
1089 $self->_LoadConfig(%args);
1095 my %args = ( File => '', @_ );
1097 my ($is_ext, $is_site);
1098 if ( $args{'File'} eq ($ENV{RT_SITE_CONFIG}||'') ) {
1099 ($is_ext, $is_site) = ('', 1);
1101 $is_ext = $args{'File'} =~ /^(?!RT_)(?:(.*)_)(?:Site)?Config/ ? $1 : '';
1102 $is_site = $args{'File'} =~ /SiteConfig/ ? 1 : 0;
1107 local *Set = sub(\[$@%]@) {
1108 my ( $opt_ref, @args ) = @_;
1109 my ( $pack, $file, $line ) = caller;
1110 return $self->SetFromConfig(
1116 SiteConfig => $is_site,
1117 Extension => $is_ext,
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],
1130 SiteConfig => $is_site,
1131 Extension => $is_ext,
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'};
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$@};
1146 my $username = getpwuid($>);
1147 my $group = getgrgid($();
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 ) {
1158 unless ($file_path) {
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$@};
1164 my $message = <<EOF;
1166 RT couldn't load RT config file %s as:
1170 The file is owned by user %s and group %s.
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.
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$@";
1185 # Loaded successfully
1186 push @LOADED_CONFIGS, {
1187 as => $args{'File'},
1188 filename => $INC{ $args{'File'} },
1189 extension => $is_ext,
1198 foreach my $o ( grep $META{$_}{'PostLoadCheck'}, $self->Options( Overridable => undef ) ) {
1199 $META{$o}->{'PostLoadCheck'}->( $self, $self->Get($o) );
1205 Returns list of config files found in local etc, plugins' etc
1206 and main etc directories.
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;
1224 @configs = grep !$seen{$_}++, @configs;
1228 =head2 LoadedConfigs
1230 Returns a list of hashrefs, one for each config file loaded. The keys of the
1237 Name this config file was loaded as (relative filename usually).
1241 The full path and filename.
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>.
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>.
1258 # Copy to avoid the caller changing our internal data
1259 return map { \%$_ } @LOADED_CONFIGS
1264 Takes name of the option as argument and returns its current value.
1266 In the case of a user-overridable option, first checks the user's
1267 preferences before looking for site-wide configuration.
1269 Returns values from RT_SiteConfig, RT_Config and then the %META hash
1270 of configuration variables's "Default" for this config variable,
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.
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'), ...)>.
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)>.
1293 my ( $self, $name, $user ) = @_;
1296 if ( $user && $user->id && $META{$name}->{'Overridable'} ) {
1297 my $prefs = $user->Preferences($RT::System);
1298 $res = $prefs->{$name} if $prefs;
1300 $res = $OPTIONS{$name} unless defined $res;
1301 $res = $META{$name}->{'Default'} unless defined $res;
1302 return $self->_ReturnValue( $res, $META{$name}->{'Type'} || 'SCALAR' );
1305 =head2 GetObfuscated
1307 the same as Get, except it returns Obfuscated value via Obfuscate sub
1313 my ( $name, $user ) = @_;
1314 my $obfuscate = $META{$name}->{Obfuscate};
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
1319 return $self->Get(@_) unless $obfuscate;
1321 my $res = $self->Get(@_);
1322 $res = $obfuscate->( $self, $res, $user );
1323 return $self->_ReturnValue( $res, $META{$name}->{'Type'} || 'SCALAR' );
1328 Set option's value to new value. Takes name of the option and new value.
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
1338 my ( $self, $name ) = ( shift, shift );
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"} = (@_); }
1349 $OPTIONS{$name} = shift;
1350 {no warnings 'once'; no strict 'refs'; ${"RT::$name"} = $OPTIONS{$name}; }
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'}}
1363 $META{$name}->{'PostLoadCheck'} ||= sub {
1365 Message => "Configuration option $name is deprecated",
1371 return $self->_ReturnValue( $old, $type );
1375 my ( $self, $res, $type ) = @_;
1376 return $res unless wantarray;
1378 if ( $type eq 'ARRAY' ) {
1379 return @{ $res || [] };
1380 } elsif ( $type eq 'HASH' ) {
1381 return %{ $res || {} };
1399 unless ( $args{'File'} ) {
1400 ( $args{'Package'}, $args{'File'}, $args{'Line'} ) = caller(1);
1403 my $opt = $args{'Option'};
1406 my $name = Symbol::Global::Name->find($opt);
1412 $type = $META{$name}->{'Type'} || 'SCALAR';
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' ) {
1420 @{ $args{'Value'} },
1421 @{ $args{'Value'} }%2? (undef) : (),
1422 $self->Get( $name ),
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'} };
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')
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
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
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'}."
1457 $META{$name}->{'Type'} = $type;
1458 foreach (qw(Package File Line SiteConfig Extension)) {
1459 $META{$name}->{'Source'}->{$_} = $args{$_};
1461 $self->Set( $name, @{ $args{'Value'} } );
1474 return $META{ $_[1] };
1482 map $_->{'Section'} || 'General',
1489 my %args = ( Section => undef, Overridable => 1, Sorted => 1, @_ );
1490 my @res = sort keys %META;
1492 @res = grep( ( $META{$_}->{'Section'} || 'General' ) eq $args{'Section'},
1494 ) if defined $args{'Section'};
1496 if ( defined $args{'Overridable'} ) {
1498 = grep( ( $META{$_}->{'Overridable'} || 0 ) == $args{'Overridable'},
1502 if ( $args{'Sorted'} ) {
1504 ($META{$a}->{SortOrder}||9999) <=> ($META{$b}->{SortOrder}||9999)
1508 @res = sort { $a cmp $b } @res;
1513 =head2 AddOption( Name => '', Section => '', ... )
1524 Widget => '/Widgets/Form/String',
1525 WidgetArguments => {},
1529 unless ( $args{Name} ) {
1530 $RT::Logger->error("Need Name to add a new config");
1534 unless ( $args{Section} ) {
1535 $RT::Logger->error("Need Section to add a new config option");
1539 $META{ delete $args{Name} } = \%args;
1542 =head2 DeleteOption( Name => '' )
1552 if ( $args{Name} ) {
1553 delete $META{$args{Name}};
1556 $RT::Logger->error("Need Name to remove a config option");
1561 =head2 UpdateOption( Name => '' ), Section => '', ... )
1570 Overridable => undef,
1573 WidgetArguments => undef,
1577 my $name = delete $args{Name};
1580 $RT::Logger->error("Need Name to update a new config");
1584 unless ( exists $META{$name} ) {
1585 $RT::Logger->error("Config $name doesn't exist");
1589 for my $type ( keys %args ) {
1590 next unless defined $args{$type};
1591 $META{$name}{$type} = $args{$type};
1596 RT::Base->_ImportOverlays();