1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2017 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) {
343 $labels{$value} = ['Refresh search results every [quant,_1,minute,minutes].', $value / 60]; # loc
346 $labels{$value} = ['Refresh search results every [quant,_1,second,seconds].', $value]; # loc
352 return { Values => \@values, ValuesLabel => \%labels };
357 # User overridable options for RT at a glance
358 HomePageRefreshInterval => {
359 Section => 'RT at a glance', #loc
362 Widget => '/Widgets/Form/Select',
364 Description => 'Home page refresh interval', #loc
366 my @values = RT->Config->Get('RefreshIntervals');
368 0 => "Don't refresh home page.", # loc
371 for my $value (@values) {
372 if ($value % 60 == 0) {
373 $labels{$value} = ['Refresh home page every [quant,_1,minute,minutes].', $value / 60]; # loc
376 $labels{$value} = ['Refresh home page every [quant,_1,second,seconds].', $value]; # loc
382 return { Values => \@values, ValuesLabel => \%labels };
387 # User overridable options for Ticket displays
389 Section => 'Ticket display', # loc
392 Widget => '/Widgets/Form/Boolean',
394 Description => 'Display messages in rich text if available', # loc
395 Hints => 'Rich text (HTML) shows formatting such as colored text, bold, italics, and more', # loc
399 Section => 'Ticket display', #loc
402 Widget => '/Widgets/Form/Integer',
404 Description => 'Maximum inline message length', #loc
406 "Length in characters; Use '0' to show all messages inline, regardless of length" #loc
409 OldestTransactionsFirst => {
410 Section => 'Ticket display',
413 Widget => '/Widgets/Form/Boolean',
415 Description => 'Show oldest history first', #loc
419 Section => 'Ticket display',
422 Widget => '/Widgets/Form/Select',
424 Description => 'Show history', #loc
425 Values => [qw(delay click always)],
427 delay => "after the rest of the page loads", #loc
428 click => "after clicking a link", #loc
429 always => "immediately", #loc
433 ShowUnreadMessageNotifications => {
434 Section => 'Ticket display',
437 Widget => '/Widgets/Form/Boolean',
439 Description => 'Notify me of unread messages', #loc
447 $self->SetFromConfig(
448 Option => \'PlainTextMono',
450 %{$self->Meta('PlainTextPre')->{'Source'}}
453 PostLoadCheck => sub {
455 # XXX: deprecated, remove in 4.4
456 $RT::Logger->info("You set \$PlainTextPre in your config, which has been removed in favor of \$PlainTextMono. Please update your config.")
457 if $self->Meta('PlainTextPre')->{'Source'}{'Package'};
461 Section => 'Ticket display',
464 Widget => '/Widgets/Form/Boolean',
466 Description => 'Display plain-text attachments in fixed-width font', #loc
467 Hints => 'Display all plain-text attachments in a monospace font with formatting preserved, but wrapping as needed.', #loc
470 MoreAboutRequestorTicketList => {
471 Section => 'Ticket display', #loc
474 Widget => '/Widgets/Form/Select',
476 Description => 'What tickets to display in the "More about requestor" box', #loc
477 Values => [qw(Active Inactive All None)],
479 Active => "Show the Requestor's 10 highest priority active tickets", #loc
480 Inactive => "Show the Requestor's 10 highest priority inactive tickets", #loc
481 All => "Show the Requestor's 10 highest priority tickets", #loc
482 None => "Show no tickets for the Requestor", #loc
486 SimplifiedRecipients => {
487 Section => 'Ticket display', #loc
490 Widget => '/Widgets/Form/Boolean',
492 Description => "Show simplified recipient list on ticket update", #loc
495 DisplayTicketAfterQuickCreate => {
496 Section => 'Ticket display',
499 Widget => '/Widgets/Form/Boolean',
501 Description => 'Display ticket after "Quick Create"', #loc
505 Section => 'Ticket display',
508 Widget => '/Widgets/Form/Boolean',
510 Description => 'Enable quote folding?' # loc
514 # User overridable locale options
516 Section => 'Locale', #loc
518 Widget => '/Widgets/Form/Select',
520 Description => 'Date format', #loc
521 Callback => sub { my $ret = { Values => [], ValuesLabel => {}};
522 my $date = RT::Date->new($HTML::Mason::Commands::session{'CurrentUser'});
524 foreach my $value ($date->Formatters) {
525 push @{$ret->{Values}}, $value;
526 $ret->{ValuesLabel}{$value} = $date->Get(
538 PostLoadCheck => sub {
540 my $value = $self->Get('RTAddressRegexp');
543 'The RTAddressRegexp option is not set in the config.'
544 .' Not setting this option results in additional SQL queries to'
545 .' check whether each address belongs to RT or not.'
546 .' It is especially important to set this option if RT receives'
547 .' emails on addresses that are not in the database or config.'
549 } elsif (ref $value and ref $value eq "Regexp") {
550 # Ensure that the regex is case-insensitive; while the
551 # local part of email addresses is _technically_
552 # case-sensitive, most MTAs don't treat it as such.
553 $RT::Logger->warning(
554 'RTAddressRegexp is set to a case-sensitive regular expression.'
555 .' This may lead to mail loops with MTAs which treat the'
556 .' local part as case-insensitive -- which is most of them.'
557 ) if "$value" =~ /^\(\?[a-z]*-([a-z]*):/ and "$1" =~ /i/;
561 # User overridable mail options
563 Section => 'Mail', #loc
565 Default => 'Individual messages',
566 Widget => '/Widgets/Form/Select',
568 Description => 'Email delivery', #loc
570 'Individual messages', #loc
572 'Weekly digest', #loc
578 Section => 'Mail', #loc
581 Widget => '/Widgets/Form/Boolean',
583 Description => 'Outgoing mail', #loc
584 Hints => 'Should RT send you mail for ticket updates you make?', #loc
588 # this tends to break extensions that stash links in ticket update pages
591 PostLoadCheck => sub {
592 my ($self,$value) = @_;
593 $RT::Logger->error("your \$Organization setting ($value) appears to contain whitespace. Please fix this.")
598 # Internal config options
599 DatabaseExtraDSN => {
605 PostLoadCheck => sub {
607 my $v = $self->Get('FullTextSearch');
608 return unless $v->{Enable} and $v->{Indexed};
609 my $dbtype = $self->Get('DatabaseType');
610 if ($dbtype eq 'Oracle') {
611 if (not $v->{IndexName}) {
612 $RT::Logger->error("No IndexName set for full-text index; disabling");
613 $v->{Enable} = $v->{Indexed} = 0;
615 } elsif ($dbtype eq 'Pg') {
617 if (not $v->{'Column'}) {
618 $RT::Logger->error("No Column set for full-text index; disabling");
619 $v->{Enable} = $v->{Indexed} = 0;
620 } elsif ($v->{'Column'} eq "Content"
621 and (not $v->{'Table'} or $v->{'Table'} eq "Attachments")) {
622 $RT::Logger->error("Column for full-text index is set to Content, not tsvector column; disabling");
623 $v->{Enable} = $v->{Indexed} = 0;
625 } elsif ($dbtype eq 'mysql') {
626 if (not $v->{'Table'}) {
627 $RT::Logger->error("No Table set for full-text index; disabling");
628 $v->{Enable} = $v->{Indexed} = 0;
629 } elsif ($v->{'Table'} eq "Attachments") {
630 $RT::Logger->error("Table for full-text index is set to Attachments, not FTS table; disabling");
631 $v->{Enable} = $v->{Indexed} = 0;
633 my (undef, $create) = eval { $RT::Handle->dbh->selectrow_array("SHOW CREATE TABLE " . $v->{Table}); };
634 my ($engine) = ($create||'') =~ /engine=(\S+)/i;
636 $RT::Logger->error("External table ".$v->{Table}." does not exist");
637 $v->{Enable} = $v->{Indexed} = 0;
638 } elsif (lc $engine eq "sphinx") {
639 # External Sphinx indexer
641 unless ($v->{'MaxMatches'}) {
642 $RT::Logger->warn("No MaxMatches set for full-text index; defaulting to 10000");
643 $v->{MaxMatches} = 10_000;
646 # Internal, one-column table
647 $v->{Column} = 'Content';
648 $v->{Engine} = $engine;
652 $RT::Logger->error("Indexed full-text-search not supported for $dbtype");
659 PostLoadCheck => sub {
663 return if GraphViz->require;
664 $RT::Logger->debug("You've enabled GraphViz, but we couldn't load the module: $@");
665 $self->Set( DisableGraphViz => 1 );
670 PostLoadCheck => sub {
674 return if GD->require;
675 $RT::Logger->debug("You've enabled GD, but we couldn't load the module: $@");
676 $self->Set( DisableGD => 1 );
681 PostLoadCheck => sub {
683 my $value = $self->Get('MailCommand');
684 return if ref($value) eq "CODE"
685 or $value =~/^(sendmail|sendmailpipe|qmail|testfile|mbox)$/;
686 $RT::Logger->error("Unknown value for \$MailCommand: $value; defaulting to sendmailpipe");
687 $self->Set( MailCommand => 'sendmailpipe' );
692 PostLoadCheck => sub { RT::Interface::Email->_HTMLFormatter },
696 PostLoadCheck => sub {
699 # Make sure Crypt is post-loaded first
700 $META{Crypt}{'PostLoadCheck'}->( $self, $self->Get( 'Crypt' ) );
702 my @plugins = $self->Get('MailPlugins');
703 if ( grep $_ eq 'Auth::GnuPG' || $_ eq 'Auth::SMIME', @plugins ) {
704 $RT::Logger->warning(
705 'Auth::GnuPG and Auth::SMIME (from an extension) have been'
706 .' replaced with Auth::Crypt. @MailPlugins has been adjusted,'
707 .' but should be updated to replace both with Auth::Crypt to'
708 .' silence this warning.'
714 $_ eq 'Auth::GnuPG' || $_ eq 'Auth::SMIME'
717 $self->Set( MailPlugins => @plugins );
720 if ( not @{$self->Get('Crypt')->{Incoming}} and grep $_ eq 'Auth::Crypt', @plugins ) {
721 $RT::Logger->warning("Auth::Crypt enabled in MailPlugins, but no available incoming encryption formats");
727 PostLoadCheck => sub {
731 for my $proto (RT::Crypt->EnabledProtocols) {
732 my $opt = $self->Get($proto);
733 if (not RT::Crypt->LoadImplementation($proto)) {
734 $RT::Logger->error("You enabled $proto, but we couldn't load module RT::Crypt::$proto");
735 $opt->{'Enable'} = 0;
736 } elsif (not RT::Crypt->LoadImplementation($proto)->Probe) {
737 $opt->{'Enable'} = 0;
738 } elsif ($META{$proto}{'PostLoadCheck'}) {
739 $META{$proto}{'PostLoadCheck'}->( $self, $self->Get( $proto ) );
744 my $opt = $self->Get('Crypt');
745 my @enabled = RT::Crypt->EnabledProtocols;
747 $enabled{$_} = 1 for @enabled;
748 $opt->{'Enable'} = scalar @enabled;
749 $opt->{'Incoming'} = [ $opt->{'Incoming'} ]
750 if $opt->{'Incoming'} and not ref $opt->{'Incoming'};
751 if ( $opt->{'Incoming'} && @{ $opt->{'Incoming'} } ) {
752 $RT::Logger->warning("$_ explicitly set as incoming Crypt plugin, but not marked Enabled; removing")
753 for grep {not $enabled{$_}} @{$opt->{'Incoming'}};
754 $opt->{'Incoming'} = [ grep {$enabled{$_}} @{$opt->{'Incoming'}} ];
756 $opt->{'Incoming'} = \@enabled;
758 if ( $opt->{'Outgoing'} ) {
759 if (not $enabled{$opt->{'Outgoing'}}) {
760 $RT::Logger->warning($opt->{'Outgoing'}.
761 " explicitly set as outgoing Crypt plugin, but not marked Enabled; "
762 . (@enabled ? "using $enabled[0]" : "removing"));
764 $opt->{'Outgoing'} = $enabled[0] unless $enabled{$opt->{'Outgoing'}};
766 $opt->{'Outgoing'} = $enabled[0];
772 PostLoadCheck => sub {
774 my $opt = $self->Get('SMIME');
775 return unless $opt->{'Enable'};
777 if (exists $opt->{Keyring}) {
778 unless ( File::Spec->file_name_is_absolute( $opt->{Keyring} ) ) {
779 $opt->{Keyring} = File::Spec->catfile( $RT::BasePath, $opt->{Keyring} );
781 unless (-d $opt->{Keyring} and -r _) {
783 "RT's SMIME libraries couldn't successfully read your".
784 " configured SMIME keyring directory (".$opt->{Keyring}
786 delete $opt->{Keyring};
790 if (defined $opt->{CAPath}) {
791 if (-d $opt->{CAPath} and -r _) {
793 } elsif (-f $opt->{CAPath} and -r _) {
797 "RT's SMIME libraries could not read your configured CAPath (".$opt->{CAPath}.")"
799 delete $opt->{CAPath};
806 PostLoadCheck => sub {
808 my $gpg = $self->Get('GnuPG');
809 return unless $gpg->{'Enable'};
811 my $gpgopts = $self->Get('GnuPGOptions');
812 unless ( File::Spec->file_name_is_absolute( $gpgopts->{homedir} ) ) {
813 $gpgopts->{homedir} = File::Spec->catfile( $RT::BasePath, $gpgopts->{homedir} );
815 unless (-d $gpgopts->{homedir} && -r _ ) { # no homedir, no gpg
817 "RT's GnuPG libraries couldn't successfully read your".
818 " configured GnuPG home directory (".$gpgopts->{homedir}
819 ."). GnuPG support has been disabled");
820 $gpg->{'Enable'} = 0;
824 if ( grep exists $gpg->{$_}, qw(RejectOnMissingPrivateKey RejectOnBadData AllowEncryptDataInDB) ) {
825 $RT::Logger->warning(
826 "The RejectOnMissingPrivateKey, RejectOnBadData and AllowEncryptDataInDB"
827 ." GnuPG options are now properties of the generic Crypt configuration. You"
828 ." should set them there instead."
830 delete $gpg->{$_} for qw(RejectOnMissingPrivateKey RejectOnBadData AllowEncryptDataInDB);
834 GnuPGOptions => { Type => 'HASH' },
835 ReferrerWhitelist => { Type => 'ARRAY' },
837 PostLoadCheck => sub {
841 # "In most cases, you should leave $WebPath set to '' (an empty value)."
842 return unless $value;
844 # try to catch someone who assumes that you shouldn't leave this empty
846 $RT::Logger->error("For the WebPath config option, use the empty string instead of /");
850 # $WebPath requires a leading / but no trailing /, or it can be blank.
851 return if $value =~ m{^/.+[^/]$};
853 if ($value =~ m{/$}) {
854 $RT::Logger->error("The WebPath config option requires no trailing slash");
857 if ($value !~ m{^/}) {
858 $RT::Logger->error("The WebPath config option requires a leading slash");
863 PostLoadCheck => sub {
868 $RT::Logger->error("You must set the WebDomain config option");
872 if ($value =~ m{^(\w+://)}) {
873 $RT::Logger->error("The WebDomain config option must not contain a scheme ($1)");
877 if ($value =~ m{(/.*)}) {
878 $RT::Logger->error("The WebDomain config option must not contain a path ($1)");
882 if ($value =~ m{:(\d*)}) {
883 $RT::Logger->error("The WebDomain config option must not contain a port ($1)");
889 PostLoadCheck => sub {
894 $RT::Logger->error("You must set the WebPort config option");
898 if ($value !~ m{^\d+$}) {
899 $RT::Logger->error("The WebPort config option must be an integer");
904 PostLoadCheck => sub {
909 $RT::Logger->error("You must set the WebBaseURL config option");
913 if ($value !~ m{^https?://}i) {
914 $RT::Logger->error("The WebBaseURL config option must contain a scheme (http or https)");
917 if ($value =~ m{/$}) {
918 $RT::Logger->error("The WebBaseURL config option requires no trailing slash");
921 if ($value =~ m{^https?://.+?(/[^/].*)}i) {
922 $RT::Logger->error("The WebBaseURL config option must not contain a path ($1)");
927 PostLoadCheck => sub {
932 $RT::Logger->error("You must set the WebURL config option");
936 if ($value !~ m{^https?://}i) {
937 $RT::Logger->error("The WebURL config option must contain a scheme (http or https)");
940 if ($value !~ m{/$}) {
941 $RT::Logger->error("The WebURL config option requires a trailing slash");
945 EmailInputEncodings => {
947 PostLoadCheck => sub {
949 my $value = $self->Get('EmailInputEncodings');
950 return unless $value && @$value;
953 foreach my $encoding ( grep defined && length, splice @$value ) {
954 next if $seen{ $encoding };
955 if ( $encoding eq '*' ) {
956 unshift @$value, '*';
960 my $canonic = Encode::resolve_alias( $encoding );
961 unless ( $canonic ) {
962 warn "Unknown encoding '$encoding' in \@EmailInputEncodings option";
964 elsif ( $seen{ $canonic }++ ) {
968 push @$value, $canonic;
975 Instead => 'LogToSTDERR',
979 UserAutocompleteFields => {
981 Instead => 'UserSearchFields',
985 CustomFieldGroupings => {
987 PostLoadCheck => sub {
989 # use scalar context intentionally to avoid not a hash error
990 my $groups = $config->Get('CustomFieldGroupings') || {};
992 unless (ref($groups) eq 'HASH') {
993 RT->Logger->error("Config option \%CustomFieldGroupings is a @{[ref $groups]} not a HASH; ignoring");
997 for my $class (keys %$groups) {
999 if (ref($groups->{$class}) eq 'HASH') {
1000 push @h, $_, $groups->{$class}->{$_}
1001 for sort {lc($a) cmp lc($b)} keys %{ $groups->{$class} };
1002 } elsif (ref($groups->{$class}) eq 'ARRAY') {
1003 @h = @{ $groups->{$class} };
1005 RT->Logger->error("Config option \%CustomFieldGroupings{$class} is not a HASH or ARRAY; ignoring");
1006 delete $groups->{$class};
1010 $groups->{$class} = [];
1012 my $group = shift @h;
1014 if (ref($ref) eq 'ARRAY') {
1015 push @{$groups->{$class}}, $group => $ref;
1017 RT->Logger->error("Config option \%CustomFieldGroupings{$class}{$group} is not an ARRAY; ignoring");
1021 $config->Set( CustomFieldGroupings => %$groups );
1027 WebExternalAuth => { Deprecated => { Instead => 'WebRemoteUserAuth', Remove => '4.4' }},
1028 WebExternalAuthContinuous => { Deprecated => { Instead => 'WebRemoteUserContinuous', Remove => '4.4' }},
1029 WebFallbackToInternalAuth => { Deprecated => { Instead => 'WebFallbackToRTLogin', Remove => '4.4' }},
1030 WebExternalGecos => { Deprecated => { Instead => 'WebRemoteUserGecos', Remove => '4.4' }},
1031 WebExternalAuto => { Deprecated => { Instead => 'WebRemoteUserAutocreate', Remove => '4.4' }},
1032 AutoCreate => { Deprecated => { Instead => 'UserAutocreateDefaultsOnLogin', Remove => '4.4' }},
1033 LogoImageHeight => {
1036 Message => "The LogoImageHeight configuration option did not affect display, and has been removed; please remove it from your RT_SiteConfig.pm",
1042 Message => "The LogoImageWidth configuration option did not affect display, and has been removed; please remove it from your RT_SiteConfig.pm",
1045 DatabaseRequireSSL => {
1049 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.",
1054 my @LOADED_CONFIGS = ();
1060 Object constructor returns new object. Takes no arguments.
1066 my $class = ref($proto) ? ref($proto) : $proto;
1067 my $self = bless {}, $class;
1078 Load all configs. First of all load RT's config then load
1079 extensions' config files in alphabetical order.
1087 $self->LoadConfig( File => 'RT_Config.pm' );
1089 my @configs = $self->Configs;
1090 $self->LoadConfig( File => $_ ) foreach @configs;
1096 Takes param hash with C<File> field.
1097 First, the site configuration file is loaded, in order to establish
1098 overall site settings like hostname and name of RT instance.
1099 Then, the core configuration file is loaded to set fallback values
1100 for all settings; it bases some values on settings from the site
1103 B<Note> that core config file don't change options if site config
1104 has set them so to add value to some option instead of
1105 overriding you have to copy original value from core config file.
1111 my %args = ( File => '', @_ );
1112 $args{'File'} =~ s/(?<!Site)(?=Config\.pm$)/Site/;
1113 if ( $args{'File'} eq 'RT_SiteConfig.pm'
1114 and my $site_config = $ENV{RT_SITE_CONFIG} )
1116 $self->_LoadConfig( %args, File => $site_config );
1117 # to allow load siteconfig again and again in case it's updated
1118 delete $INC{ $site_config };
1120 $self->_LoadConfig(%args);
1121 delete $INC{$args{'File'}};
1124 $args{'File'} =~ s/Site(?=Config\.pm$)//;
1125 $self->_LoadConfig(%args);
1131 my %args = ( File => '', @_ );
1133 my ($is_ext, $is_site);
1134 if ( $args{'File'} eq ($ENV{RT_SITE_CONFIG}||'') ) {
1135 ($is_ext, $is_site) = ('', 1);
1137 $is_ext = $args{'File'} =~ /^(?!RT_)(?:(.*)_)(?:Site)?Config/ ? $1 : '';
1138 $is_site = $args{'File'} =~ /SiteConfig/ ? 1 : 0;
1143 local *Set = sub(\[$@%]@) {
1144 my ( $opt_ref, @args ) = @_;
1145 my ( $pack, $file, $line ) = caller;
1146 return $self->SetFromConfig(
1152 SiteConfig => $is_site,
1153 Extension => $is_ext,
1156 local *Plugin = sub {
1157 my (@new_plugins) = @_;
1158 @new_plugins = map {s/-/::/g if not /:/; $_} @new_plugins;
1159 my ( $pack, $file, $line ) = caller;
1160 return $self->SetFromConfig(
1161 Option => \@RT::Plugins,
1162 Value => [@RT::Plugins, @new_plugins],
1166 SiteConfig => $is_site,
1167 Extension => $is_ext,
1170 my @etc_dirs = ($RT::LocalEtcPath);
1171 push @etc_dirs, RT->PluginDirs('etc') if $is_ext;
1172 push @etc_dirs, $RT::EtcPath, @INC;
1173 local @INC = @etc_dirs;
1174 require $args{'File'};
1177 return 1 if $is_site && $@ =~ /^Can't locate \Q$args{File}/;
1178 if ( $is_site || $@ !~ /^Can't locate \Q$args{File}/ ) {
1179 die qq{Couldn't load RT config file $args{'File'}:\n\n$@};
1182 my $username = getpwuid($>);
1183 my $group = getgrgid($();
1185 my ( $file_path, $fileuid, $filegid );
1186 foreach ( $RT::LocalEtcPath, $RT::EtcPath, @INC ) {
1187 my $tmp = File::Spec->catfile( $_, $args{File} );
1188 ( $fileuid, $filegid ) = ( stat($tmp) )[ 4, 5 ];
1189 if ( defined $fileuid ) {
1194 unless ($file_path) {
1196 qq{Couldn't load RT config file $args{'File'} as user $username / group $group.\n}
1197 . qq{The file couldn't be found in $RT::LocalEtcPath and $RT::EtcPath.\n$@};
1200 my $message = <<EOF;
1202 RT couldn't load RT config file %s as:
1206 The file is owned by user %s and group %s.
1208 This usually means that the user/group your webserver is running
1209 as cannot read the file. Be careful not to make the permissions
1210 on this file too liberal, because it contains database passwords.
1211 You may need to put the webserver user in the appropriate group
1212 (%s) or change permissions be able to run succesfully.
1215 my $fileusername = getpwuid($fileuid);
1216 my $filegroup = getgrgid($filegid);
1217 my $errormessage = sprintf( $message,
1218 $file_path, $fileusername, $filegroup, $filegroup );
1219 die "$errormessage\n$@";
1221 # Loaded successfully
1222 push @LOADED_CONFIGS, {
1223 as => $args{'File'},
1224 filename => $INC{ $args{'File'} },
1225 extension => $is_ext,
1234 foreach my $o ( grep $META{$_}{'PostLoadCheck'}, $self->Options( Overridable => undef ) ) {
1235 $META{$o}->{'PostLoadCheck'}->( $self, $self->Get($o) );
1241 Returns list of config files found in local etc, plugins' etc
1242 and main etc directories.
1250 foreach my $path ( $RT::LocalEtcPath, RT->PluginDirs('etc'), $RT::EtcPath ) {
1251 my $mask = File::Spec->catfile( $path, "*_Config.pm" );
1252 my @files = glob $mask;
1253 @files = grep !/^RT_Config\.pm$/,
1254 grep $_ && /^\w+_Config\.pm$/,
1255 map { s/^.*[\\\/]//; $_ } @files;
1256 push @configs, sort @files;
1260 @configs = grep !$seen{$_}++, @configs;
1264 =head2 LoadedConfigs
1266 Returns a list of hashrefs, one for each config file loaded. The keys of the
1273 Name this config file was loaded as (relative filename usually).
1277 The full path and filename.
1281 The "extension" part of the filename. For example, the file C<RTIR_Config.pm>
1282 will have an C<extension> value of C<RTIR>.
1286 True if the file is considered a site-level override. For example, C<site>
1287 will be false for C<RT_Config.pm> and true for C<RT_SiteConfig.pm>.
1294 # Copy to avoid the caller changing our internal data
1295 return map { \%$_ } @LOADED_CONFIGS
1300 Takes name of the option as argument and returns its current value.
1302 In the case of a user-overridable option, first checks the user's
1303 preferences before looking for site-wide configuration.
1305 Returns values from RT_SiteConfig, RT_Config and then the %META hash
1306 of configuration variables's "Default" for this config variable,
1309 Returns different things in scalar and array contexts. For scalar
1310 options it's not that important, however for arrays and hash it's.
1311 In scalar context returns references to arrays and hashes.
1313 Use C<scalar> perl's op to force context, especially when you use
1314 C<(..., Argument => RT->Config->Get('ArrayOpt'), ...)>
1315 as perl's '=>' op doesn't change context of the right hand argument to
1316 scalar. Instead use C<(..., Argument => scalar RT->Config->Get('ArrayOpt'), ...)>.
1318 It's also important for options that have no default value(no default
1319 in F<etc/RT_Config.pm>). If you don't force scalar context then you'll
1320 get empty list and all your named args will be messed up. For example
1321 C<(arg1 => 1, arg2 => RT->Config->Get('OptionDoesNotExist'), arg3 => 3)>
1322 will result in C<(arg1 => 1, arg2 => 'arg3', 3)> what is most probably
1323 unexpected, or C<(arg1 => 1, arg2 => RT->Config->Get('ArrayOption'), arg3 => 3)>
1324 will result in C<(arg1 => 1, arg2 => 'element of option', 'another_one' => ..., 'arg3', 3)>.
1329 my ( $self, $name, $user ) = @_;
1332 if ( $user && $user->id && $META{$name}->{'Overridable'} ) {
1333 my $prefs = $user->Preferences($RT::System);
1334 $res = $prefs->{$name} if $prefs;
1336 $res = $OPTIONS{$name} unless defined $res;
1337 $res = $META{$name}->{'Default'} unless defined $res;
1338 return $self->_ReturnValue( $res, $META{$name}->{'Type'} || 'SCALAR' );
1341 =head2 GetObfuscated
1343 the same as Get, except it returns Obfuscated value via Obfuscate sub
1349 my ( $name, $user ) = @_;
1350 my $obfuscate = $META{$name}->{Obfuscate};
1352 # we use two Get here is to simplify the logic of the return value
1353 # configs need obfuscation are supposed to be less, so won't be too heavy
1355 return $self->Get(@_) unless $obfuscate;
1357 my $res = $self->Get(@_);
1358 $res = $obfuscate->( $self, $res, $user );
1359 return $self->_ReturnValue( $res, $META{$name}->{'Type'} || 'SCALAR' );
1364 Set option's value to new value. Takes name of the option and new value.
1367 The new value should be scalar, array or hash depending on type of the option.
1368 If the option is not defined in meta or the default RT config then it is of
1374 my ( $self, $name ) = ( shift, shift );
1376 my $old = $OPTIONS{$name};
1377 my $type = $META{$name}->{'Type'} || 'SCALAR';
1378 if ( $type eq 'ARRAY' ) {
1379 $OPTIONS{$name} = [@_];
1380 { no warnings 'once'; no strict 'refs'; @{"RT::$name"} = (@_); }
1381 } elsif ( $type eq 'HASH' ) {
1382 $OPTIONS{$name} = {@_};
1383 { no warnings 'once'; no strict 'refs'; %{"RT::$name"} = (@_); }
1385 $OPTIONS{$name} = shift;
1386 {no warnings 'once'; no strict 'refs'; ${"RT::$name"} = $OPTIONS{$name}; }
1388 $META{$name}->{'Type'} = $type;
1389 $META{$name}->{'PostSet'}->($self, $OPTIONS{$name}, $old)
1390 if $META{$name}->{'PostSet'};
1391 if ($META{$name}->{'Deprecated'}) {
1392 my %deprecated = %{$META{$name}->{'Deprecated'}};
1393 my $new_var = $deprecated{Instead} || '';
1394 $self->SetFromConfig(
1395 Option => \$new_var,
1396 Value => [$OPTIONS{$name}],
1397 %{$self->Meta($name)->{'Source'}}
1399 $META{$name}->{'PostLoadCheck'} ||= sub {
1401 Message => "Configuration option $name is deprecated",
1407 return $self->_ReturnValue( $old, $type );
1411 my ( $self, $res, $type ) = @_;
1412 return $res unless wantarray;
1414 if ( $type eq 'ARRAY' ) {
1415 return @{ $res || [] };
1416 } elsif ( $type eq 'HASH' ) {
1417 return %{ $res || {} };
1435 unless ( $args{'File'} ) {
1436 ( $args{'Package'}, $args{'File'}, $args{'Line'} ) = caller(1);
1439 my $opt = $args{'Option'};
1442 my $name = Symbol::Global::Name->find($opt);
1448 $type = $META{$name}->{'Type'} || 'SCALAR';
1451 # if option is already set we have to check where
1452 # it comes from and may be ignore it
1453 if ( exists $OPTIONS{$name} ) {
1454 if ( $type eq 'HASH' ) {
1456 @{ $args{'Value'} },
1457 @{ $args{'Value'} }%2? (undef) : (),
1458 $self->Get( $name ),
1460 } elsif ( $args{'SiteConfig'} && $args{'Extension'} ) {
1461 # if it's site config of an extension then it can only
1462 # override options that came from its main config
1463 if ( $args{'Extension'} ne $META{$name}->{'Source'}{'Extension'} ) {
1464 my %source = %{ $META{$name}->{'Source'} };
1466 "Change of config option '$name' at $args{'File'} line $args{'Line'} has been ignored."
1467 ." This option earlier has been set in $source{'File'} line $source{'Line'}."
1468 ." To overide this option use ". ($source{'Extension'}||'RT')
1473 } elsif ( !$args{'SiteConfig'} && $META{$name}->{'Source'}{'SiteConfig'} ) {
1474 # if it's core config then we can override any option that came from another
1475 # core config, but not site config
1477 my %source = %{ $META{$name}->{'Source'} };
1478 if ( $source{'Extension'} ne $args{'Extension'} ) {
1479 # as a site config is loaded earlier then its base config
1480 # then we warn only on different extensions, for example
1481 # RTIR's options is set in main site config
1483 "Change of config option '$name' at $args{'File'} line $args{'Line'} has been ignored."
1484 ." It may be ok, but we want you to be aware."
1485 ." This option has been set earlier in $source{'File'} line $source{'Line'}."
1493 $META{$name}->{'Type'} = $type;
1494 foreach (qw(Package File Line SiteConfig Extension)) {
1495 $META{$name}->{'Source'}->{$_} = $args{$_};
1497 $self->Set( $name, @{ $args{'Value'} } );
1510 return $META{ $_[1] };
1518 map $_->{'Section'} || 'General',
1525 my %args = ( Section => undef, Overridable => 1, Sorted => 1, @_ );
1526 my @res = sort keys %META;
1528 @res = grep( ( $META{$_}->{'Section'} || 'General' ) eq $args{'Section'},
1530 ) if defined $args{'Section'};
1532 if ( defined $args{'Overridable'} ) {
1534 = grep( ( $META{$_}->{'Overridable'} || 0 ) == $args{'Overridable'},
1538 if ( $args{'Sorted'} ) {
1540 ($META{$a}->{SortOrder}||9999) <=> ($META{$b}->{SortOrder}||9999)
1544 @res = sort { $a cmp $b } @res;
1549 =head2 AddOption( Name => '', Section => '', ... )
1560 Widget => '/Widgets/Form/String',
1561 WidgetArguments => {},
1565 unless ( $args{Name} ) {
1566 $RT::Logger->error("Need Name to add a new config");
1570 unless ( $args{Section} ) {
1571 $RT::Logger->error("Need Section to add a new config option");
1575 $META{ delete $args{Name} } = \%args;
1578 =head2 DeleteOption( Name => '' )
1588 if ( $args{Name} ) {
1589 delete $META{$args{Name}};
1592 $RT::Logger->error("Need Name to remove a config option");
1597 =head2 UpdateOption( Name => '' ), Section => '', ... )
1606 Overridable => undef,
1609 WidgetArguments => undef,
1613 my $name = delete $args{Name};
1616 $RT::Logger->error("Need Name to update a new config");
1620 unless ( exists $META{$name} ) {
1621 $RT::Logger->error("Config $name doesn't exist");
1625 for my $type ( keys %args ) {
1626 next unless defined $args{$type};
1627 $META{$name}{$type} = $args{$type};
1632 RT::Base->_ImportOverlays();