1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2019 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
146 RestrictReferrerLogin => {
147 PostLoadCheck => sub {
149 if (defined($self->Get('RestrictReferrerLogin'))) {
150 RT::Logger->error("The config option 'RestrictReferrerLogin' is incorrect, and should be 'RestrictLoginReferrer' instead.");
155 Section => 'General',
158 Widget => '/Widgets/Form/Select',
160 Description => 'Default queue', #loc
162 my $ret = { Values => [], ValuesLabel => {}};
163 my $q = RT::Queues->new($HTML::Mason::Commands::session{'CurrentUser'});
165 while (my $queue = $q->Next) {
166 next unless $queue->CurrentUserHasRight("CreateTicket");
167 push @{$ret->{Values}}, $queue->Id;
168 $ret->{ValuesLabel}{$queue->Id} = $queue->Name;
174 RememberDefaultQueue => {
175 Section => 'General',
178 Widget => '/Widgets/Form/Boolean',
180 Description => 'Remember default queue' # loc
184 Section => 'General',
187 Widget => '/Widgets/Form/Select',
189 Description => 'Username format', # loc
190 Values => [qw(role concise verbose)],
192 role => 'Privileged: usernames; Unprivileged: names and email addresses', # loc
193 concise => 'Short usernames', # loc
194 verbose => 'Name and email address', # loc
198 AutocompleteOwners => {
199 Section => 'General',
202 Widget => '/Widgets/Form/Boolean',
204 Description => 'Use autocomplete to find owners?', # loc
205 Hints => 'Replaces the owner dropdowns with textboxes' #loc
208 WebDefaultStylesheet => {
209 Section => 'General', #loc
212 Widget => '/Widgets/Form/Select',
214 Description => 'Theme', #loc
217 unless (@stylesheets) {
218 for my $static_path ( RT::Interface::Web->StaticRoots ) {
220 File::Spec->catdir( $static_path, 'css' );
221 next unless -d $css_path;
222 if ( opendir my $dh, $css_path ) {
223 push @stylesheets, grep {
224 $_ ne 'base' && -e File::Spec->catfile( $css_path, $_, 'main.css' )
228 RT->Logger->error("Can't read $css_path: $!");
231 @stylesheets = sort { lc $a cmp lc $b } uniq @stylesheets;
233 return { Values => \@stylesheets };
236 PostLoadCheck => sub {
238 my $value = $self->Get('WebDefaultStylesheet');
240 my @roots = RT::Interface::Web->StaticRoots;
241 for my $root (@roots) {
242 return if -d "$root/css/$value";
245 $RT::Logger->warning(
246 "The default stylesheet ($value) does not exist in this instance of RT. "
247 . "Defaulting to freeside4."
250 $self->Set('WebDefaultStylesheet', 'freeside4');
254 Section => 'General',
257 Widget => '/Widgets/Form/Boolean',
259 Description => 'Include time in iCal feed events?', # loc
260 Hints => 'Formats iCal feed events with date and time' #loc
263 UseSideBySideLayout => {
264 Section => 'Ticket composition',
267 Widget => '/Widgets/Form/Boolean',
269 Description => 'Use a two column layout for create and update forms?' # loc
272 MessageBoxRichText => {
273 Section => 'Ticket composition',
276 Widget => '/Widgets/Form/Boolean',
278 Description => 'WYSIWYG message composer' # loc
281 MessageBoxRichTextHeight => {
282 Section => 'Ticket composition',
285 Widget => '/Widgets/Form/Integer',
287 Description => 'WYSIWYG composer height', # loc
291 Section => 'Ticket composition',
294 Widget => '/Widgets/Form/Integer',
296 Description => 'Message box width', #loc
299 MessageBoxHeight => {
300 Section => 'Ticket composition',
303 Widget => '/Widgets/Form/Integer',
305 Description => 'Message box height', #loc
308 DefaultTimeUnitsToHours => {
309 Section => 'Ticket composition', #loc
312 Widget => '/Widgets/Form/Boolean',
314 Description => 'Enter time in hours by default', #loc
315 Hints => 'Only for entry, not display', #loc
318 RefreshIntervals => {
320 PostLoadCheck => sub {
322 my @intervals = $self->Get('RefreshIntervals');
323 if (grep { $_ == 0 } @intervals) {
324 $RT::Logger->warning("Please do not include a 0 value in RefreshIntervals, as that default is already added for you.");
328 SearchResultsRefreshInterval => {
329 Section => 'General', #loc
332 Widget => '/Widgets/Form/Select',
334 Description => 'Search results refresh interval', #loc
336 my @values = RT->Config->Get('RefreshIntervals');
338 0 => "Don't refresh search results.", # loc
341 for my $value (@values) {
342 if ($value % 60 == 0) {
344 'Refresh search results every [quant,_1,minute,minutes].', #loc
350 'Refresh search results every [quant,_1,second,seconds].', #loc
358 return { Values => \@values, ValuesLabel => \%labels };
363 # User overridable options for RT at a glance
364 HomePageRefreshInterval => {
365 Section => 'RT at a glance', #loc
368 Widget => '/Widgets/Form/Select',
370 Description => 'Home page refresh interval', #loc
372 my @values = RT->Config->Get('RefreshIntervals');
374 0 => "Don't refresh home page.", # loc
377 for my $value (@values) {
378 if ($value % 60 == 0) {
380 'Refresh home page every [quant,_1,minute,minutes].', #loc
386 'Refresh home page every [quant,_1,second,seconds].', #loc
394 return { Values => \@values, ValuesLabel => \%labels };
399 # User overridable options for Ticket displays
401 Section => 'Ticket display', # loc
404 Widget => '/Widgets/Form/Boolean',
406 Description => 'Display messages in rich text if available', # loc
407 Hints => 'Rich text (HTML) shows formatting such as colored text, bold, italics, and more', # loc
411 Section => 'Ticket display', #loc
414 Widget => '/Widgets/Form/Integer',
416 Description => 'Maximum inline message length', #loc
418 "Length in characters; Use '0' to show all messages inline, regardless of length" #loc
421 OldestTransactionsFirst => {
422 Section => 'Ticket display',
425 Widget => '/Widgets/Form/Boolean',
427 Description => 'Show oldest history first', #loc
431 Section => 'Ticket display',
434 Widget => '/Widgets/Form/Select',
436 Description => 'Show history', #loc
437 Values => [qw(delay click always)],
439 delay => "after the rest of the page loads", #loc
440 click => "after clicking a link", #loc
441 always => "immediately", #loc
445 ShowUnreadMessageNotifications => {
446 Section => 'Ticket display',
449 Widget => '/Widgets/Form/Boolean',
451 Description => 'Notify me of unread messages', #loc
459 $self->SetFromConfig(
460 Option => \'PlainTextMono',
462 %{$self->Meta('PlainTextPre')->{'Source'}}
465 PostLoadCheck => sub {
467 # XXX: deprecated, remove in 4.4
468 $RT::Logger->info("You set \$PlainTextPre in your config, which has been removed in favor of \$PlainTextMono. Please update your config.")
469 if $self->Meta('PlainTextPre')->{'Source'}{'Package'};
473 Section => 'Ticket display',
476 Widget => '/Widgets/Form/Boolean',
478 Description => 'Display plain-text attachments in fixed-width font', #loc
479 Hints => 'Display all plain-text attachments in a monospace font with formatting preserved, but wrapping as needed.', #loc
482 MoreAboutRequestorTicketList => {
483 Section => 'Ticket display', #loc
486 Widget => '/Widgets/Form/Select',
488 Description => 'What tickets to display in the "More about requestor" box', #loc
489 Values => [qw(Active Inactive All None)],
491 Active => "Show the Requestor's 10 highest priority active tickets", #loc
492 Inactive => "Show the Requestor's 10 highest priority inactive tickets", #loc
493 All => "Show the Requestor's 10 highest priority tickets", #loc
494 None => "Show no tickets for the Requestor", #loc
498 SimplifiedRecipients => {
499 Section => 'Ticket display', #loc
502 Widget => '/Widgets/Form/Boolean',
504 Description => "Show simplified recipient list on ticket update", #loc
507 DisplayTicketAfterQuickCreate => {
508 Section => 'Ticket display',
511 Widget => '/Widgets/Form/Boolean',
513 Description => 'Display ticket after "Quick Create"', #loc
517 Section => 'Ticket display',
520 Widget => '/Widgets/Form/Boolean',
522 Description => 'Enable quote folding?' # loc
526 # User overridable locale options
528 Section => 'Locale', #loc
530 Widget => '/Widgets/Form/Select',
532 Description => 'Date format', #loc
533 Callback => sub { my $ret = { Values => [], ValuesLabel => {}};
534 my $date = RT::Date->new($HTML::Mason::Commands::session{'CurrentUser'});
536 foreach my $value ($date->Formatters) {
537 push @{$ret->{Values}}, $value;
538 $ret->{ValuesLabel}{$value} = $date->Get(
550 PostLoadCheck => sub {
552 my $value = $self->Get('RTAddressRegexp');
555 'The RTAddressRegexp option is not set in the config.'
556 .' Not setting this option results in additional SQL queries to'
557 .' check whether each address belongs to RT or not.'
558 .' It is especially important to set this option if RT receives'
559 .' emails on addresses that are not in the database or config.'
561 } elsif (ref $value and ref $value eq "Regexp") {
562 # Ensure that the regex is case-insensitive; while the
563 # local part of email addresses is _technically_
564 # case-sensitive, most MTAs don't treat it as such.
565 $RT::Logger->warning(
566 'RTAddressRegexp is set to a case-sensitive regular expression.'
567 .' This may lead to mail loops with MTAs which treat the'
568 .' local part as case-insensitive -- which is most of them.'
569 ) if "$value" =~ /^\(\?[a-z]*-([a-z]*):/ and "$1" =~ /i/;
573 # User overridable mail options
575 Section => 'Mail', #loc
577 Default => 'Individual messages',
578 Widget => '/Widgets/Form/Select',
580 Description => 'Email delivery', #loc
582 'Individual messages', #loc
584 'Weekly digest', #loc
590 Section => 'Mail', #loc
593 Widget => '/Widgets/Form/Boolean',
595 Description => 'Outgoing mail', #loc
596 Hints => 'Should RT send you mail for ticket updates you make?', #loc
600 # this tends to break extensions that stash links in ticket update pages
603 PostLoadCheck => sub {
604 my ($self,$value) = @_;
605 $RT::Logger->error("your \$Organization setting ($value) appears to contain whitespace. Please fix this.")
610 # Internal config options
611 DatabaseExtraDSN => {
617 PostLoadCheck => sub {
619 my $v = $self->Get('FullTextSearch');
620 return unless $v->{Enable} and $v->{Indexed};
621 my $dbtype = $self->Get('DatabaseType');
622 if ($dbtype eq 'Oracle') {
623 if (not $v->{IndexName}) {
624 $RT::Logger->error("No IndexName set for full-text index; disabling");
625 $v->{Enable} = $v->{Indexed} = 0;
627 } elsif ($dbtype eq 'Pg') {
629 if (not $v->{'Column'}) {
630 $RT::Logger->error("No Column set for full-text index; disabling");
631 $v->{Enable} = $v->{Indexed} = 0;
632 } elsif ($v->{'Column'} eq "Content"
633 and (not $v->{'Table'} or $v->{'Table'} eq "Attachments")) {
634 $RT::Logger->error("Column for full-text index is set to Content, not tsvector column; disabling");
635 $v->{Enable} = $v->{Indexed} = 0;
637 } elsif ($dbtype eq 'mysql') {
638 if (not $v->{'Table'}) {
639 $RT::Logger->error("No Table set for full-text index; disabling");
640 $v->{Enable} = $v->{Indexed} = 0;
641 } elsif ($v->{'Table'} eq "Attachments") {
642 $RT::Logger->error("Table for full-text index is set to Attachments, not FTS table; disabling");
643 $v->{Enable} = $v->{Indexed} = 0;
645 my (undef, $create) = eval { $RT::Handle->dbh->selectrow_array("SHOW CREATE TABLE " . $v->{Table}); };
646 my ($engine) = ($create||'') =~ /engine=(\S+)/i;
648 $RT::Logger->error("External table ".$v->{Table}." does not exist");
649 $v->{Enable} = $v->{Indexed} = 0;
650 } elsif (lc $engine eq "sphinx") {
651 # External Sphinx indexer
653 unless ($v->{'MaxMatches'}) {
654 $RT::Logger->warn("No MaxMatches set for full-text index; defaulting to 10000");
655 $v->{MaxMatches} = 10_000;
658 # Internal, one-column table
659 $v->{Column} = 'Content';
660 $v->{Engine} = $engine;
664 $RT::Logger->error("Indexed full-text-search not supported for $dbtype");
671 PostLoadCheck => sub {
675 return if GraphViz->require;
676 $RT::Logger->debug("You've enabled GraphViz, but we couldn't load the module: $@");
677 $self->Set( DisableGraphViz => 1 );
682 PostLoadCheck => sub {
686 return if GD->require;
687 $RT::Logger->debug("You've enabled GD, but we couldn't load the module: $@");
688 $self->Set( DisableGD => 1 );
693 PostLoadCheck => sub {
695 my $value = $self->Get('MailCommand');
696 return if ref($value) eq "CODE"
697 or $value =~/^(sendmail|sendmailpipe|qmail|testfile|mbox)$/;
698 $RT::Logger->error("Unknown value for \$MailCommand: $value; defaulting to sendmailpipe");
699 $self->Set( MailCommand => 'sendmailpipe' );
704 PostLoadCheck => sub { RT::Interface::Email->_HTMLFormatter },
708 PostLoadCheck => sub {
711 # Make sure Crypt is post-loaded first
712 $META{Crypt}{'PostLoadCheck'}->( $self, $self->Get( 'Crypt' ) );
714 my @plugins = $self->Get('MailPlugins');
715 if ( grep $_ eq 'Auth::GnuPG' || $_ eq 'Auth::SMIME', @plugins ) {
716 $RT::Logger->warning(
717 'Auth::GnuPG and Auth::SMIME (from an extension) have been'
718 .' replaced with Auth::Crypt. @MailPlugins has been adjusted,'
719 .' but should be updated to replace both with Auth::Crypt to'
720 .' silence this warning.'
726 $_ eq 'Auth::GnuPG' || $_ eq 'Auth::SMIME'
729 $self->Set( MailPlugins => @plugins );
732 if ( not @{$self->Get('Crypt')->{Incoming}} and grep $_ eq 'Auth::Crypt', @plugins ) {
733 $RT::Logger->warning("Auth::Crypt enabled in MailPlugins, but no available incoming encryption formats");
739 PostLoadCheck => sub {
743 for my $proto (RT::Crypt->EnabledProtocols) {
744 my $opt = $self->Get($proto);
745 if (not RT::Crypt->LoadImplementation($proto)) {
746 $RT::Logger->error("You enabled $proto, but we couldn't load module RT::Crypt::$proto");
747 $opt->{'Enable'} = 0;
748 } elsif (not RT::Crypt->LoadImplementation($proto)->Probe) {
749 $opt->{'Enable'} = 0;
750 } elsif ($META{$proto}{'PostLoadCheck'}) {
751 $META{$proto}{'PostLoadCheck'}->( $self, $self->Get( $proto ) );
756 my $opt = $self->Get('Crypt');
757 my @enabled = RT::Crypt->EnabledProtocols;
759 $enabled{$_} = 1 for @enabled;
760 $opt->{'Enable'} = scalar @enabled;
761 $opt->{'Incoming'} = [ $opt->{'Incoming'} ]
762 if $opt->{'Incoming'} and not ref $opt->{'Incoming'};
763 if ( $opt->{'Incoming'} && @{ $opt->{'Incoming'} } ) {
764 $RT::Logger->warning("$_ explicitly set as incoming Crypt plugin, but not marked Enabled; removing")
765 for grep {not $enabled{$_}} @{$opt->{'Incoming'}};
766 $opt->{'Incoming'} = [ grep {$enabled{$_}} @{$opt->{'Incoming'}} ];
768 $opt->{'Incoming'} = \@enabled;
770 if ( $opt->{'Outgoing'} ) {
771 if (not $enabled{$opt->{'Outgoing'}}) {
772 $RT::Logger->warning($opt->{'Outgoing'}.
773 " explicitly set as outgoing Crypt plugin, but not marked Enabled; "
774 . (@enabled ? "using $enabled[0]" : "removing"));
776 $opt->{'Outgoing'} = $enabled[0] unless $enabled{$opt->{'Outgoing'}};
778 $opt->{'Outgoing'} = $enabled[0];
784 PostLoadCheck => sub {
786 my $opt = $self->Get('SMIME');
787 return unless $opt->{'Enable'};
789 if (exists $opt->{Keyring}) {
790 unless ( File::Spec->file_name_is_absolute( $opt->{Keyring} ) ) {
791 $opt->{Keyring} = File::Spec->catfile( $RT::BasePath, $opt->{Keyring} );
793 unless (-d $opt->{Keyring} and -r _) {
795 "RT's SMIME libraries couldn't successfully read your".
796 " configured SMIME keyring directory (".$opt->{Keyring}
798 delete $opt->{Keyring};
802 if (defined $opt->{CAPath}) {
803 if (-d $opt->{CAPath} and -r _) {
805 } elsif (-f $opt->{CAPath} and -r _) {
809 "RT's SMIME libraries could not read your configured CAPath (".$opt->{CAPath}.")"
811 delete $opt->{CAPath};
818 PostLoadCheck => sub {
820 my $gpg = $self->Get('GnuPG');
821 return unless $gpg->{'Enable'};
823 my $gpgopts = $self->Get('GnuPGOptions');
824 unless ( File::Spec->file_name_is_absolute( $gpgopts->{homedir} ) ) {
825 $gpgopts->{homedir} = File::Spec->catfile( $RT::BasePath, $gpgopts->{homedir} );
827 unless (-d $gpgopts->{homedir} && -r _ ) { # no homedir, no gpg
829 "RT's GnuPG libraries couldn't successfully read your".
830 " configured GnuPG home directory (".$gpgopts->{homedir}
831 ."). GnuPG support has been disabled");
832 $gpg->{'Enable'} = 0;
836 if ( grep exists $gpg->{$_}, qw(RejectOnMissingPrivateKey RejectOnBadData AllowEncryptDataInDB) ) {
837 $RT::Logger->warning(
838 "The RejectOnMissingPrivateKey, RejectOnBadData and AllowEncryptDataInDB"
839 ." GnuPG options are now properties of the generic Crypt configuration. You"
840 ." should set them there instead."
842 delete $gpg->{$_} for qw(RejectOnMissingPrivateKey RejectOnBadData AllowEncryptDataInDB);
846 GnuPGOptions => { Type => 'HASH' },
847 ReferrerWhitelist => { Type => 'ARRAY' },
849 PostLoadCheck => sub {
853 # "In most cases, you should leave $WebPath set to '' (an empty value)."
854 return unless $value;
856 # try to catch someone who assumes that you shouldn't leave this empty
858 $RT::Logger->error("For the WebPath config option, use the empty string instead of /");
862 # $WebPath requires a leading / but no trailing /, or it can be blank.
863 return if $value =~ m{^/.+[^/]$};
865 if ($value =~ m{/$}) {
866 $RT::Logger->error("The WebPath config option requires no trailing slash");
869 if ($value !~ m{^/}) {
870 $RT::Logger->error("The WebPath config option requires a leading slash");
875 PostLoadCheck => sub {
880 $RT::Logger->error("You must set the WebDomain config option");
884 if ($value =~ m{^(\w+://)}) {
885 $RT::Logger->error("The WebDomain config option must not contain a scheme ($1)");
889 if ($value =~ m{(/.*)}) {
890 $RT::Logger->error("The WebDomain config option must not contain a path ($1)");
894 if ($value =~ m{:(\d*)}) {
895 $RT::Logger->error("The WebDomain config option must not contain a port ($1)");
901 PostLoadCheck => sub {
906 $RT::Logger->error("You must set the WebPort config option");
910 if ($value !~ m{^\d+$}) {
911 $RT::Logger->error("The WebPort config option must be an integer");
916 PostLoadCheck => sub {
921 $RT::Logger->error("You must set the WebBaseURL config option");
925 if ($value !~ m{^https?://}i) {
926 $RT::Logger->error("The WebBaseURL config option must contain a scheme (http or https)");
929 if ($value =~ m{/$}) {
930 $RT::Logger->error("The WebBaseURL config option requires no trailing slash");
933 if ($value =~ m{^https?://.+?(/[^/].*)}i) {
934 $RT::Logger->error("The WebBaseURL config option must not contain a path ($1)");
939 PostLoadCheck => sub {
944 $RT::Logger->error("You must set the WebURL config option");
948 if ($value !~ m{^https?://}i) {
949 $RT::Logger->error("The WebURL config option must contain a scheme (http or https)");
952 if ($value !~ m{/$}) {
953 $RT::Logger->error("The WebURL config option requires a trailing slash");
957 EmailInputEncodings => {
959 PostLoadCheck => sub {
961 my $value = $self->Get('EmailInputEncodings');
962 return unless $value && @$value;
965 foreach my $encoding ( grep defined && length, splice @$value ) {
966 next if $seen{ $encoding };
967 if ( $encoding eq '*' ) {
968 unshift @$value, '*';
972 my $canonic = Encode::resolve_alias( $encoding );
973 unless ( $canonic ) {
974 warn "Unknown encoding '$encoding' in \@EmailInputEncodings option";
976 elsif ( $seen{ $canonic }++ ) {
980 push @$value, $canonic;
987 Instead => 'LogToSTDERR',
991 UserAutocompleteFields => {
993 Instead => 'UserSearchFields',
997 CustomFieldGroupings => {
999 PostLoadCheck => sub {
1001 # use scalar context intentionally to avoid not a hash error
1002 my $groups = $config->Get('CustomFieldGroupings') || {};
1004 unless (ref($groups) eq 'HASH') {
1005 RT->Logger->error("Config option \%CustomFieldGroupings is a @{[ref $groups]} not a HASH; ignoring");
1009 for my $class (keys %$groups) {
1011 if (ref($groups->{$class}) eq 'HASH') {
1012 push @h, $_, $groups->{$class}->{$_}
1013 for sort {lc($a) cmp lc($b)} keys %{ $groups->{$class} };
1014 } elsif (ref($groups->{$class}) eq 'ARRAY') {
1015 @h = @{ $groups->{$class} };
1017 RT->Logger->error("Config option \%CustomFieldGroupings{$class} is not a HASH or ARRAY; ignoring");
1018 delete $groups->{$class};
1022 $groups->{$class} = [];
1024 my $group = shift @h;
1026 if (ref($ref) eq 'ARRAY') {
1027 push @{$groups->{$class}}, $group => $ref;
1029 RT->Logger->error("Config option \%CustomFieldGroupings{$class}{$group} is not an ARRAY; ignoring");
1033 $config->Set( CustomFieldGroupings => %$groups );
1039 WebExternalAuth => { Deprecated => { Instead => 'WebRemoteUserAuth', Remove => '4.4' }},
1040 WebExternalAuthContinuous => { Deprecated => { Instead => 'WebRemoteUserContinuous', Remove => '4.4' }},
1041 WebFallbackToInternalAuth => { Deprecated => { Instead => 'WebFallbackToRTLogin', Remove => '4.4' }},
1042 WebExternalGecos => { Deprecated => { Instead => 'WebRemoteUserGecos', Remove => '4.4' }},
1043 WebExternalAuto => { Deprecated => { Instead => 'WebRemoteUserAutocreate', Remove => '4.4' }},
1044 AutoCreate => { Deprecated => { Instead => 'UserAutocreateDefaultsOnLogin', Remove => '4.4' }},
1045 LogoImageHeight => {
1048 Message => "The LogoImageHeight configuration option did not affect display, and has been removed; please remove it from your RT_SiteConfig.pm",
1054 Message => "The LogoImageWidth configuration option did not affect display, and has been removed; please remove it from your RT_SiteConfig.pm",
1057 DatabaseRequireSSL => {
1061 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.",
1066 my @LOADED_CONFIGS = ();
1072 Object constructor returns new object. Takes no arguments.
1078 my $class = ref($proto) ? ref($proto) : $proto;
1079 my $self = bless {}, $class;
1090 Load all configs. First of all load RT's config then load
1091 extensions' config files in alphabetical order.
1099 $self->LoadConfig( File => 'RT_Config.pm' );
1101 my @configs = $self->Configs;
1102 $self->LoadConfig( File => $_ ) foreach @configs;
1108 Takes param hash with C<File> field.
1109 First, the site configuration file is loaded, in order to establish
1110 overall site settings like hostname and name of RT instance.
1111 Then, the core configuration file is loaded to set fallback values
1112 for all settings; it bases some values on settings from the site
1115 B<Note> that core config file don't change options if site config
1116 has set them so to add value to some option instead of
1117 overriding you have to copy original value from core config file.
1123 my %args = ( File => '', @_ );
1124 $args{'File'} =~ s/(?<!Site)(?=Config\.pm$)/Site/;
1125 if ( $args{'File'} eq 'RT_SiteConfig.pm'
1126 and my $site_config = $ENV{RT_SITE_CONFIG} )
1128 $self->_LoadConfig( %args, File => $site_config );
1129 # to allow load siteconfig again and again in case it's updated
1130 delete $INC{ $site_config };
1132 $self->_LoadConfig(%args);
1133 delete $INC{$args{'File'}};
1136 $args{'File'} =~ s/Site(?=Config\.pm$)//;
1137 $self->_LoadConfig(%args);
1143 my %args = ( File => '', @_ );
1145 my ($is_ext, $is_site);
1146 if ( $args{'File'} eq ($ENV{RT_SITE_CONFIG}||'') ) {
1147 ($is_ext, $is_site) = ('', 1);
1149 $is_ext = $args{'File'} =~ /^(?!RT_)(?:(.*)_)(?:Site)?Config/ ? $1 : '';
1150 $is_site = $args{'File'} =~ /SiteConfig/ ? 1 : 0;
1155 local *Set = sub(\[$@%]@) {
1156 my ( $opt_ref, @args ) = @_;
1157 my ( $pack, $file, $line ) = caller;
1158 return $self->SetFromConfig(
1164 SiteConfig => $is_site,
1165 Extension => $is_ext,
1168 local *Plugin = sub {
1169 my (@new_plugins) = @_;
1170 @new_plugins = map {s/-/::/g if not /:/; $_} @new_plugins;
1171 my ( $pack, $file, $line ) = caller;
1172 return $self->SetFromConfig(
1173 Option => \@RT::Plugins,
1174 Value => [@RT::Plugins, @new_plugins],
1178 SiteConfig => $is_site,
1179 Extension => $is_ext,
1182 my @etc_dirs = ($RT::LocalEtcPath);
1183 push @etc_dirs, RT->PluginDirs('etc') if $is_ext;
1184 push @etc_dirs, $RT::EtcPath, @INC;
1185 local @INC = @etc_dirs;
1186 require $args{'File'};
1189 return 1 if $is_site && $@ =~ /^Can't locate \Q$args{File}/;
1190 if ( $is_site || $@ !~ /^Can't locate \Q$args{File}/ ) {
1191 die qq{Couldn't load RT config file $args{'File'}:\n\n$@};
1194 my $username = getpwuid($>);
1195 my $group = getgrgid($();
1197 my ( $file_path, $fileuid, $filegid );
1198 foreach ( $RT::LocalEtcPath, $RT::EtcPath, @INC ) {
1199 my $tmp = File::Spec->catfile( $_, $args{File} );
1200 ( $fileuid, $filegid ) = ( stat($tmp) )[ 4, 5 ];
1201 if ( defined $fileuid ) {
1206 unless ($file_path) {
1208 qq{Couldn't load RT config file $args{'File'} as user $username / group $group.\n}
1209 . qq{The file couldn't be found in $RT::LocalEtcPath and $RT::EtcPath.\n$@};
1212 my $message = <<EOF;
1214 RT couldn't load RT config file %s as:
1218 The file is owned by user %s and group %s.
1220 This usually means that the user/group your webserver is running
1221 as cannot read the file. Be careful not to make the permissions
1222 on this file too liberal, because it contains database passwords.
1223 You may need to put the webserver user in the appropriate group
1224 (%s) or change permissions be able to run succesfully.
1227 my $fileusername = getpwuid($fileuid);
1228 my $filegroup = getgrgid($filegid);
1229 my $errormessage = sprintf( $message,
1230 $file_path, $fileusername, $filegroup, $filegroup );
1231 die "$errormessage\n$@";
1233 # Loaded successfully
1234 push @LOADED_CONFIGS, {
1235 as => $args{'File'},
1236 filename => $INC{ $args{'File'} },
1237 extension => $is_ext,
1246 foreach my $o ( grep $META{$_}{'PostLoadCheck'}, $self->Options( Overridable => undef ) ) {
1247 $META{$o}->{'PostLoadCheck'}->( $self, $self->Get($o) );
1253 Returns list of config files found in local etc, plugins' etc
1254 and main etc directories.
1262 foreach my $path ( $RT::LocalEtcPath, RT->PluginDirs('etc'), $RT::EtcPath ) {
1263 my $mask = File::Spec->catfile( $path, "*_Config.pm" );
1264 my @files = glob $mask;
1265 @files = grep !/^RT_Config\.pm$/,
1266 grep $_ && /^\w+_Config\.pm$/,
1267 map { s/^.*[\\\/]//; $_ } @files;
1268 push @configs, sort @files;
1272 @configs = grep !$seen{$_}++, @configs;
1276 =head2 LoadedConfigs
1278 Returns a list of hashrefs, one for each config file loaded. The keys of the
1285 Name this config file was loaded as (relative filename usually).
1289 The full path and filename.
1293 The "extension" part of the filename. For example, the file C<RTIR_Config.pm>
1294 will have an C<extension> value of C<RTIR>.
1298 True if the file is considered a site-level override. For example, C<site>
1299 will be false for C<RT_Config.pm> and true for C<RT_SiteConfig.pm>.
1306 # Copy to avoid the caller changing our internal data
1307 return map { \%$_ } @LOADED_CONFIGS
1312 Takes name of the option as argument and returns its current value.
1314 In the case of a user-overridable option, first checks the user's
1315 preferences before looking for site-wide configuration.
1317 Returns values from RT_SiteConfig, RT_Config and then the %META hash
1318 of configuration variables's "Default" for this config variable,
1321 Returns different things in scalar and array contexts. For scalar
1322 options it's not that important, however for arrays and hash it's.
1323 In scalar context returns references to arrays and hashes.
1325 Use C<scalar> perl's op to force context, especially when you use
1326 C<(..., Argument => RT->Config->Get('ArrayOpt'), ...)>
1327 as perl's '=>' op doesn't change context of the right hand argument to
1328 scalar. Instead use C<(..., Argument => scalar RT->Config->Get('ArrayOpt'), ...)>.
1330 It's also important for options that have no default value(no default
1331 in F<etc/RT_Config.pm>). If you don't force scalar context then you'll
1332 get empty list and all your named args will be messed up. For example
1333 C<(arg1 => 1, arg2 => RT->Config->Get('OptionDoesNotExist'), arg3 => 3)>
1334 will result in C<(arg1 => 1, arg2 => 'arg3', 3)> what is most probably
1335 unexpected, or C<(arg1 => 1, arg2 => RT->Config->Get('ArrayOption'), arg3 => 3)>
1336 will result in C<(arg1 => 1, arg2 => 'element of option', 'another_one' => ..., 'arg3', 3)>.
1341 my ( $self, $name, $user ) = @_;
1344 if ( $user && $user->id && $META{$name}->{'Overridable'} ) {
1345 my $prefs = $user->Preferences($RT::System);
1346 $res = $prefs->{$name} if $prefs;
1348 $res = $OPTIONS{$name} unless defined $res;
1349 $res = $META{$name}->{'Default'} unless defined $res;
1350 return $self->_ReturnValue( $res, $META{$name}->{'Type'} || 'SCALAR' );
1353 =head2 GetObfuscated
1355 the same as Get, except it returns Obfuscated value via Obfuscate sub
1361 my ( $name, $user ) = @_;
1362 my $obfuscate = $META{$name}->{Obfuscate};
1364 # we use two Get here is to simplify the logic of the return value
1365 # configs need obfuscation are supposed to be less, so won't be too heavy
1367 return $self->Get(@_) unless $obfuscate;
1369 my $res = $self->Get(@_);
1370 $res = $obfuscate->( $self, $res, $user );
1371 return $self->_ReturnValue( $res, $META{$name}->{'Type'} || 'SCALAR' );
1376 Set option's value to new value. Takes name of the option and new value.
1379 The new value should be scalar, array or hash depending on type of the option.
1380 If the option is not defined in meta or the default RT config then it is of
1386 my ( $self, $name ) = ( shift, shift );
1388 my $old = $OPTIONS{$name};
1389 my $type = $META{$name}->{'Type'} || 'SCALAR';
1390 if ( $type eq 'ARRAY' ) {
1391 $OPTIONS{$name} = [@_];
1392 { no warnings 'once'; no strict 'refs'; @{"RT::$name"} = (@_); }
1393 } elsif ( $type eq 'HASH' ) {
1394 $OPTIONS{$name} = {@_};
1395 { no warnings 'once'; no strict 'refs'; %{"RT::$name"} = (@_); }
1397 $OPTIONS{$name} = shift;
1398 {no warnings 'once'; no strict 'refs'; ${"RT::$name"} = $OPTIONS{$name}; }
1400 $META{$name}->{'Type'} = $type;
1401 $META{$name}->{'PostSet'}->($self, $OPTIONS{$name}, $old)
1402 if $META{$name}->{'PostSet'};
1403 if ($META{$name}->{'Deprecated'}) {
1404 my %deprecated = %{$META{$name}->{'Deprecated'}};
1405 my $new_var = $deprecated{Instead} || '';
1406 $self->SetFromConfig(
1407 Option => \$new_var,
1408 Value => [$OPTIONS{$name}],
1409 %{$self->Meta($name)->{'Source'}}
1411 $META{$name}->{'PostLoadCheck'} ||= sub {
1413 Message => "Configuration option $name is deprecated",
1419 return $self->_ReturnValue( $old, $type );
1423 my ( $self, $res, $type ) = @_;
1424 return $res unless wantarray;
1426 if ( $type eq 'ARRAY' ) {
1427 return @{ $res || [] };
1428 } elsif ( $type eq 'HASH' ) {
1429 return %{ $res || {} };
1447 unless ( $args{'File'} ) {
1448 ( $args{'Package'}, $args{'File'}, $args{'Line'} ) = caller(1);
1451 my $opt = $args{'Option'};
1454 my $name = Symbol::Global::Name->find($opt);
1460 $type = $META{$name}->{'Type'} || 'SCALAR';
1463 # if option is already set we have to check where
1464 # it comes from and may be ignore it
1465 if ( exists $OPTIONS{$name} ) {
1466 if ( $type eq 'HASH' ) {
1468 @{ $args{'Value'} },
1469 @{ $args{'Value'} }%2? (undef) : (),
1470 $self->Get( $name ),
1472 } elsif ( $args{'SiteConfig'} && $args{'Extension'} ) {
1473 # if it's site config of an extension then it can only
1474 # override options that came from its main config
1475 if ( $args{'Extension'} ne $META{$name}->{'Source'}{'Extension'} ) {
1476 my %source = %{ $META{$name}->{'Source'} };
1478 "Change of config option '$name' at $args{'File'} line $args{'Line'} has been ignored."
1479 ." This option earlier has been set in $source{'File'} line $source{'Line'}."
1480 ." To overide this option use ". ($source{'Extension'}||'RT')
1485 } elsif ( !$args{'SiteConfig'} && $META{$name}->{'Source'}{'SiteConfig'} ) {
1486 # if it's core config then we can override any option that came from another
1487 # core config, but not site config
1489 my %source = %{ $META{$name}->{'Source'} };
1490 if ( $source{'Extension'} ne $args{'Extension'} ) {
1491 # as a site config is loaded earlier then its base config
1492 # then we warn only on different extensions, for example
1493 # RTIR's options is set in main site config
1495 "Change of config option '$name' at $args{'File'} line $args{'Line'} has been ignored."
1496 ." It may be ok, but we want you to be aware."
1497 ." This option has been set earlier in $source{'File'} line $source{'Line'}."
1505 $META{$name}->{'Type'} = $type;
1506 foreach (qw(Package File Line SiteConfig Extension)) {
1507 $META{$name}->{'Source'}->{$_} = $args{$_};
1509 $self->Set( $name, @{ $args{'Value'} } );
1522 return $META{ $_[1] };
1530 map $_->{'Section'} || 'General',
1537 my %args = ( Section => undef, Overridable => 1, Sorted => 1, @_ );
1538 my @res = sort keys %META;
1540 @res = grep( ( $META{$_}->{'Section'} || 'General' ) eq $args{'Section'},
1542 ) if defined $args{'Section'};
1544 if ( defined $args{'Overridable'} ) {
1546 = grep( ( $META{$_}->{'Overridable'} || 0 ) == $args{'Overridable'},
1550 if ( $args{'Sorted'} ) {
1552 ($META{$a}->{SortOrder}||9999) <=> ($META{$b}->{SortOrder}||9999)
1556 @res = sort { $a cmp $b } @res;
1561 =head2 AddOption( Name => '', Section => '', ... )
1572 Widget => '/Widgets/Form/String',
1573 WidgetArguments => {},
1577 unless ( $args{Name} ) {
1578 $RT::Logger->error("Need Name to add a new config");
1582 unless ( $args{Section} ) {
1583 $RT::Logger->error("Need Section to add a new config option");
1587 $META{ delete $args{Name} } = \%args;
1590 =head2 DeleteOption( Name => '' )
1600 if ( $args{Name} ) {
1601 delete $META{$args{Name}};
1604 $RT::Logger->error("Need Name to remove a config option");
1609 =head2 UpdateOption( Name => '' ), Section => '', ... )
1618 Overridable => undef,
1621 WidgetArguments => undef,
1625 my $name = delete $args{Name};
1628 $RT::Logger->error("Need Name to update a new config");
1632 unless ( exists $META{$name} ) {
1633 $RT::Logger->error("Config $name doesn't exist");
1637 for my $type ( keys %args ) {
1638 next unless defined $args{$type};
1639 $META{$name}{$type} = $args{$type};
1644 RT::Base->_ImportOverlays();