1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2014 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(concise verbose)],
184 concise => 'Short usernames', # loc
185 verbose => 'Name and email address', # loc
189 AutocompleteOwners => {
190 Section => 'General',
193 Widget => '/Widgets/Form/Boolean',
195 Description => 'Use autocomplete to find owners?', # loc
196 Hints => 'Replaces the owner dropdowns with textboxes' #loc
199 WebDefaultStylesheet => {
200 Section => 'General', #loc
203 Widget => '/Widgets/Form/Select',
205 Description => 'Theme', #loc
208 unless (@stylesheets) {
209 for my $static_path ( RT::Interface::Web->StaticRoots ) {
211 File::Spec->catdir( $static_path, 'css' );
212 next unless -d $css_path;
213 if ( opendir my $dh, $css_path ) {
214 push @stylesheets, grep {
215 -e File::Spec->catfile( $css_path, $_, 'base.css' )
219 RT->Logger->error("Can't read $css_path: $!");
222 @stylesheets = sort { lc $a cmp lc $b } uniq @stylesheets;
224 return { Values => \@stylesheets };
227 PostLoadCheck => sub {
229 my $value = $self->Get('WebDefaultStylesheet');
231 my @roots = RT::Interface::Web->StaticRoots;
232 for my $root (@roots) {
233 return if -d "$root/css/$value";
236 $RT::Logger->warning(
237 "The default stylesheet ($value) does not exist in this instance of RT. "
238 . "Defaulting to rudder."
241 $self->Set('WebDefaultStylesheet', 'rudder');
245 Section => 'General',
248 Widget => '/Widgets/Form/Boolean',
250 Description => 'Include time in iCal feed events?', # loc
251 Hints => 'Formats iCal feed events with date and time' #loc
254 UseSideBySideLayout => {
255 Section => 'Ticket composition',
258 Widget => '/Widgets/Form/Boolean',
260 Description => 'Use a two column layout for create and update forms?' # loc
263 MessageBoxRichText => {
264 Section => 'Ticket composition',
267 Widget => '/Widgets/Form/Boolean',
269 Description => 'WYSIWYG message composer' # loc
272 MessageBoxRichTextHeight => {
273 Section => 'Ticket composition',
276 Widget => '/Widgets/Form/Integer',
278 Description => 'WYSIWYG composer height', # loc
282 Section => 'Ticket composition',
285 Widget => '/Widgets/Form/Integer',
287 Description => 'Message box width', #loc
290 MessageBoxHeight => {
291 Section => 'Ticket composition',
294 Widget => '/Widgets/Form/Integer',
296 Description => 'Message box height', #loc
299 DefaultTimeUnitsToHours => {
300 Section => 'Ticket composition', #loc
303 Widget => '/Widgets/Form/Boolean',
305 Description => 'Enter time in hours by default', #loc
306 Hints => 'Only for entry, not display', #loc
309 SearchResultsRefreshInterval => {
310 Section => 'General', #loc
313 Widget => '/Widgets/Form/Select',
315 Description => 'Search results refresh interval', #loc
316 Values => [qw(0 120 300 600 1200 3600 7200)],
318 0 => "Don't refresh search results.", #loc
319 120 => "Refresh search results every 2 minutes.", #loc
320 300 => "Refresh search results every 5 minutes.", #loc
321 600 => "Refresh search results every 10 minutes.", #loc
322 1200 => "Refresh search results every 20 minutes.", #loc
323 3600 => "Refresh search results every 60 minutes.", #loc
324 7200 => "Refresh search results every 120 minutes.", #loc
329 # User overridable options for RT at a glance
330 HomePageRefreshInterval => {
331 Section => 'RT at a glance', #loc
334 Widget => '/Widgets/Form/Select',
336 Description => 'Home page refresh interval', #loc
337 Values => [qw(0 120 300 600 1200 3600 7200)],
339 0 => "Don't refresh home page.", #loc
340 120 => "Refresh home page every 2 minutes.", #loc
341 300 => "Refresh home page every 5 minutes.", #loc
342 600 => "Refresh home page every 10 minutes.", #loc
343 1200 => "Refresh home page every 20 minutes.", #loc
344 3600 => "Refresh home page every 60 minutes.", #loc
345 7200 => "Refresh home page every 120 minutes.", #loc
350 # User overridable options for Ticket displays
352 Section => 'Ticket display', # loc
355 Widget => '/Widgets/Form/Boolean',
357 Description => 'Display messages in rich text if available', # loc
358 Hints => 'Rich text (HTML) shows formatting such as colored text, bold, italics, and more', # loc
362 Section => 'Ticket display', #loc
365 Widget => '/Widgets/Form/Integer',
367 Description => 'Maximum inline message length', #loc
369 "Length in characters; Use '0' to show all messages inline, regardless of length" #loc
372 OldestTransactionsFirst => {
373 Section => 'Ticket display',
376 Widget => '/Widgets/Form/Boolean',
378 Description => 'Show oldest history first', #loc
382 Section => 'Ticket display',
385 Widget => '/Widgets/Form/Select',
387 Description => 'Show history', #loc
388 Values => [qw(delay click always)],
390 delay => "after the rest of the page loads", #loc
391 click => "after clicking a link", #loc
392 always => "immediately", #loc
396 ShowUnreadMessageNotifications => {
397 Section => 'Ticket display',
400 Widget => '/Widgets/Form/Boolean',
402 Description => 'Notify me of unread messages', #loc
410 $self->SetFromConfig(
411 Option => \'PlainTextMono',
413 %{$self->Meta('PlainTextPre')->{'Source'}}
416 PostLoadCheck => sub {
418 # XXX: deprecated, remove in 4.4
419 $RT::Logger->info("You set \$PlainTextPre in your config, which has been removed in favor of \$PlainTextMono. Please update your config.")
420 if $self->Meta('PlainTextPre')->{'Source'}{'Package'};
424 Section => 'Ticket display',
427 Widget => '/Widgets/Form/Boolean',
429 Description => 'Display plain-text attachments in fixed-width font', #loc
430 Hints => 'Display all plain-text attachments in a monospace font with formatting preserved, but wrapping as needed.', #loc
433 MoreAboutRequestorTicketList => {
434 Section => 'Ticket display', #loc
437 Widget => '/Widgets/Form/Select',
439 Description => 'What tickets to display in the "More about requestor" box', #loc
440 Values => [qw(Active Inactive All None)],
442 Active => "Show the Requestor's 10 highest priority active tickets", #loc
443 Inactive => "Show the Requestor's 10 highest priority inactive tickets", #loc
444 All => "Show the Requestor's 10 highest priority tickets", #loc
445 None => "Show no tickets for the Requestor", #loc
449 SimplifiedRecipients => {
450 Section => 'Ticket display', #loc
453 Widget => '/Widgets/Form/Boolean',
455 Description => "Show simplified recipient list on ticket update", #loc
458 DisplayTicketAfterQuickCreate => {
459 Section => 'Ticket display',
462 Widget => '/Widgets/Form/Boolean',
464 Description => 'Display ticket after "Quick Create"', #loc
468 Section => 'Ticket display',
471 Widget => '/Widgets/Form/Boolean',
473 Description => 'Enable quote folding?' # loc
477 # User overridable locale options
479 Section => 'Locale', #loc
481 Widget => '/Widgets/Form/Select',
483 Description => 'Date format', #loc
484 Callback => sub { my $ret = { Values => [], ValuesLabel => {}};
485 my $date = RT::Date->new($HTML::Mason::Commands::session{'CurrentUser'});
487 foreach my $value ($date->Formatters) {
488 push @{$ret->{Values}}, $value;
489 $ret->{ValuesLabel}{$value} = $date->Get(
501 PostLoadCheck => sub {
503 my $value = $self->Get('RTAddressRegexp');
506 'The RTAddressRegexp option is not set in the config.'
507 .' Not setting this option results in additional SQL queries to'
508 .' check whether each address belongs to RT or not.'
509 .' It is especially important to set this option if RT recieves'
510 .' emails on addresses that are not in the database or config.'
512 } elsif (ref $value and ref $value eq "Regexp") {
513 # Ensure that the regex is case-insensitive; while the
514 # local part of email addresses is _technically_
515 # case-sensitive, most MTAs don't treat it as such.
516 $RT::Logger->warning(
517 'RTAddressRegexp is set to a case-sensitive regular expression.'
518 .' This may lead to mail loops with MTAs which treat the'
519 .' local part as case-insensitive -- which is most of them.'
520 ) if "$value" =~ /^\(\?[a-z]*-([a-z]*):/ and "$1" =~ /i/;
524 # User overridable mail options
526 Section => 'Mail', #loc
528 Default => 'Individual messages',
529 Widget => '/Widgets/Form/Select',
531 Description => 'Email delivery', #loc
533 'Individual messages', #loc
535 'Weekly digest', #loc
541 Section => 'Mail', #loc
544 Widget => '/Widgets/Form/Boolean',
546 Description => 'Outgoing mail', #loc
547 Hints => 'Should RT send you mail for ticket updates you make?', #loc
551 # this tends to break extensions that stash links in ticket update pages
554 PostLoadCheck => sub {
555 my ($self,$value) = @_;
556 $RT::Logger->error("your \$Organization setting ($value) appears to contain whitespace. Please fix this.")
561 # Internal config options
564 PostLoadCheck => sub {
566 my $v = $self->Get('FullTextSearch');
567 return unless $v->{Enable} and $v->{Indexed};
568 my $dbtype = $self->Get('DatabaseType');
569 if ($dbtype eq 'Oracle') {
570 if (not $v->{IndexName}) {
571 $RT::Logger->error("No IndexName set for full-text index; disabling");
572 $v->{Enable} = $v->{Indexed} = 0;
574 } elsif ($dbtype eq 'Pg') {
576 if (not $v->{'Column'}) {
577 $RT::Logger->error("No Column set for full-text index; disabling");
578 $v->{Enable} = $v->{Indexed} = 0;
579 } elsif ($v->{'Column'} eq "Content"
580 and (not $v->{'Table'} or $v->{'Table'} eq "Attachments")) {
581 $RT::Logger->error("Column for full-text index is set to Content, not tsvector column; disabling");
582 $v->{Enable} = $v->{Indexed} = 0;
584 } elsif ($dbtype eq 'mysql') {
585 if (not $v->{'Table'}) {
586 $RT::Logger->error("No Table set for full-text index; disabling");
587 $v->{Enable} = $v->{Indexed} = 0;
588 } elsif ($v->{'Table'} eq "Attachments") {
589 $RT::Logger->error("Table for full-text index is set to Attachments, not SphinxSE table; disabling");
590 $v->{Enable} = $v->{Indexed} = 0;
591 } elsif (not $v->{'MaxMatches'}) {
592 $RT::Logger->warn("No MaxMatches set for full-text index; defaulting to 10000");
593 $v->{MaxMatches} = 10_000;
596 $RT::Logger->error("Indexed full-text-search not supported for $dbtype");
603 PostLoadCheck => sub {
607 return if GraphViz->require;
608 $RT::Logger->debug("You've enabled GraphViz, but we couldn't load the module: $@");
609 $self->Set( DisableGraphViz => 1 );
614 PostLoadCheck => sub {
618 return if GD->require;
619 $RT::Logger->debug("You've enabled GD, but we couldn't load the module: $@");
620 $self->Set( DisableGD => 1 );
625 PostLoadCheck => sub {
627 my $value = $self->Get('MailCommand');
628 return if ref($value) eq "CODE"
629 or $value =~/^(sendmail|sendmailpipe|qmail|testfile)$/;
630 $RT::Logger->error("Unknown value for \$MailCommand: $value; defaulting to sendmailpipe");
631 $self->Set( MailCommand => 'sendmailpipe' );
636 PostLoadCheck => sub {
639 # Make sure Crypt is post-loaded first
640 $META{Crypt}{'PostLoadCheck'}->( $self, $self->Get( 'Crypt' ) );
642 my @plugins = $self->Get('MailPlugins');
643 if ( grep $_ eq 'Auth::GnuPG' || $_ eq 'Auth::SMIME', @plugins ) {
644 $RT::Logger->warning(
645 'Auth::GnuPG and Auth::SMIME (from an extension) have been'
646 .' replaced with Auth::Crypt. @MailPlugins has been adjusted,'
647 .' but should be updated to replace both with Auth::Crypt to'
648 .' silence this warning.'
654 $_ eq 'Auth::GnuPG' || $_ eq 'Auth::SMIME'
657 $self->Set( MailPlugins => @plugins );
660 if ( not @{$self->Get('Crypt')->{Incoming}} and grep $_ eq 'Auth::Crypt', @plugins ) {
661 $RT::Logger->warning("Auth::Crypt enabled in MailPlugins, but no available incoming encryption formats");
667 PostLoadCheck => sub {
671 for my $proto (RT::Crypt->EnabledProtocols) {
672 my $opt = $self->Get($proto);
673 if (not RT::Crypt->LoadImplementation($proto)) {
674 $RT::Logger->error("You enabled $proto, but we couldn't load module RT::Crypt::$proto");
675 $opt->{'Enable'} = 0;
676 } elsif (not RT::Crypt->LoadImplementation($proto)->Probe) {
677 $opt->{'Enable'} = 0;
678 } elsif ($META{$proto}{'PostLoadCheck'}) {
679 $META{$proto}{'PostLoadCheck'}->( $self, $self->Get( $proto ) );
684 my $opt = $self->Get('Crypt');
685 my @enabled = RT::Crypt->EnabledProtocols;
687 $enabled{$_} = 1 for @enabled;
688 $opt->{'Enable'} = scalar @enabled;
689 $opt->{'Incoming'} = [ $opt->{'Incoming'} ]
690 if $opt->{'Incoming'} and not ref $opt->{'Incoming'};
691 if ( $opt->{'Incoming'} && @{ $opt->{'Incoming'} } ) {
692 $RT::Logger->warning("$_ explicitly set as incoming Crypt plugin, but not marked Enabled; removing")
693 for grep {not $enabled{$_}} @{$opt->{'Incoming'}};
694 $opt->{'Incoming'} = [ grep {$enabled{$_}} @{$opt->{'Incoming'}} ];
696 $opt->{'Incoming'} = \@enabled;
698 if ( $opt->{'Outgoing'} ) {
699 if (not $enabled{$opt->{'Outgoing'}}) {
700 $RT::Logger->warning($opt->{'Outgoing'}.
701 " explicitly set as outgoing Crypt plugin, but not marked Enabled; "
702 . (@enabled ? "using $enabled[0]" : "removing"));
704 $opt->{'Outgoing'} = $enabled[0] unless $enabled{$opt->{'Outgoing'}};
706 $opt->{'Outgoing'} = $enabled[0];
712 PostLoadCheck => sub {
714 my $opt = $self->Get('SMIME');
715 return unless $opt->{'Enable'};
717 if (exists $opt->{Keyring}) {
718 unless ( File::Spec->file_name_is_absolute( $opt->{Keyring} ) ) {
719 $opt->{Keyring} = File::Spec->catfile( $RT::BasePath, $opt->{Keyring} );
721 unless (-d $opt->{Keyring} and -r _) {
723 "RT's SMIME libraries couldn't successfully read your".
724 " configured SMIME keyring directory (".$opt->{Keyring}
726 delete $opt->{Keyring};
730 if (defined $opt->{CAPath}) {
731 if (-d $opt->{CAPath} and -r _) {
733 } elsif (-f $opt->{CAPath} and -r _) {
737 "RT's SMIME libraries could not read your configured CAPath (".$opt->{CAPath}.")"
739 delete $opt->{CAPath};
746 PostLoadCheck => sub {
748 my $gpg = $self->Get('GnuPG');
749 return unless $gpg->{'Enable'};
751 my $gpgopts = $self->Get('GnuPGOptions');
752 unless ( File::Spec->file_name_is_absolute( $gpgopts->{homedir} ) ) {
753 $gpgopts->{homedir} = File::Spec->catfile( $RT::BasePath, $gpgopts->{homedir} );
755 unless (-d $gpgopts->{homedir} && -r _ ) { # no homedir, no gpg
757 "RT's GnuPG libraries couldn't successfully read your".
758 " configured GnuPG home directory (".$gpgopts->{homedir}
759 ."). GnuPG support has been disabled");
760 $gpg->{'Enable'} = 0;
764 if ( grep exists $gpg->{$_}, qw(RejectOnMissingPrivateKey RejectOnBadData AllowEncryptDataInDB) ) {
765 $RT::Logger->warning(
766 "The RejectOnMissingPrivateKey, RejectOnBadData and AllowEncryptDataInDB"
767 ." GnuPG options are now properties of the generic Crypt configuration. You"
768 ." should set them there instead."
770 delete $gpg->{$_} for qw(RejectOnMissingPrivateKey RejectOnBadData AllowEncryptDataInDB);
774 GnuPGOptions => { Type => 'HASH' },
775 ReferrerWhitelist => { Type => 'ARRAY' },
777 PostLoadCheck => sub {
781 # "In most cases, you should leave $WebPath set to '' (an empty value)."
782 return unless $value;
784 # try to catch someone who assumes that you shouldn't leave this empty
786 $RT::Logger->error("For the WebPath config option, use the empty string instead of /");
790 # $WebPath requires a leading / but no trailing /, or it can be blank.
791 return if $value =~ m{^/.+[^/]$};
793 if ($value =~ m{/$}) {
794 $RT::Logger->error("The WebPath config option requires no trailing slash");
797 if ($value !~ m{^/}) {
798 $RT::Logger->error("The WebPath config option requires a leading slash");
803 PostLoadCheck => sub {
808 $RT::Logger->error("You must set the WebDomain config option");
812 if ($value =~ m{^(\w+://)}) {
813 $RT::Logger->error("The WebDomain config option must not contain a scheme ($1)");
817 if ($value =~ m{(/.*)}) {
818 $RT::Logger->error("The WebDomain config option must not contain a path ($1)");
822 if ($value =~ m{:(\d*)}) {
823 $RT::Logger->error("The WebDomain config option must not contain a port ($1)");
829 PostLoadCheck => sub {
834 $RT::Logger->error("You must set the WebPort config option");
838 if ($value !~ m{^\d+$}) {
839 $RT::Logger->error("The WebPort config option must be an integer");
844 PostLoadCheck => sub {
849 $RT::Logger->error("You must set the WebBaseURL config option");
853 if ($value !~ m{^https?://}i) {
854 $RT::Logger->error("The WebBaseURL config option must contain a scheme (http or https)");
857 if ($value =~ m{/$}) {
858 $RT::Logger->error("The WebBaseURL config option requires no trailing slash");
861 if ($value =~ m{^https?://.+?(/[^/].*)}i) {
862 $RT::Logger->error("The WebBaseURL config option must not contain a path ($1)");
867 PostLoadCheck => sub {
872 $RT::Logger->error("You must set the WebURL config option");
876 if ($value !~ m{^https?://}i) {
877 $RT::Logger->error("The WebURL config option must contain a scheme (http or https)");
880 if ($value !~ m{/$}) {
881 $RT::Logger->error("The WebURL config option requires a trailing slash");
885 EmailInputEncodings => {
887 PostLoadCheck => sub {
889 my $value = $self->Get('EmailInputEncodings');
890 return unless $value && @$value;
893 foreach my $encoding ( grep defined && length, splice @$value ) {
894 next if $seen{ $encoding };
895 if ( $encoding eq '*' ) {
896 unshift @$value, '*';
900 my $canonic = Encode::resolve_alias( $encoding );
901 unless ( $canonic ) {
902 warn "Unknown encoding '$encoding' in \@EmailInputEncodings option";
904 elsif ( $seen{ $canonic }++ ) {
908 push @$value, $canonic;
915 Instead => 'LogToSTDERR',
919 UserAutocompleteFields => {
921 Instead => 'UserSearchFields',
925 CustomFieldGroupings => {
927 PostLoadCheck => sub {
929 # use scalar context intentionally to avoid not a hash error
930 my $groups = $config->Get('CustomFieldGroupings') || {};
932 unless (ref($groups) eq 'HASH') {
933 RT->Logger->error("Config option \%CustomFieldGroupings is a @{[ref $groups]} not a HASH; ignoring");
937 for my $class (keys %$groups) {
939 if (ref($groups->{$class}) eq 'HASH') {
940 push @h, $_, $groups->{$class}->{$_}
941 for sort {lc($a) cmp lc($b)} keys %{ $groups->{$class} };
942 } elsif (ref($groups->{$class}) eq 'ARRAY') {
943 @h = @{ $groups->{$class} };
945 RT->Logger->error("Config option \%CustomFieldGroupings{$class} is not a HASH or ARRAY; ignoring");
946 delete $groups->{$class};
950 $groups->{$class} = [];
952 my $group = shift @h;
954 if (ref($ref) eq 'ARRAY') {
955 push @{$groups->{$class}}, $group => $ref;
957 RT->Logger->error("Config option \%CustomFieldGroupings{$class}{$group} is not an ARRAY; ignoring");
961 $config->Set( CustomFieldGroupings => %$groups );
967 WebExternalAuth => { Deprecated => { Instead => 'WebRemoteUserAuth', Remove => '4.4' }},
968 WebExternalAuthContinuous => { Deprecated => { Instead => 'WebRemoteUserContinuous', Remove => '4.4' }},
969 WebFallbackToInternalAuth => { Deprecated => { Instead => 'WebFallbackToRTLogin', Remove => '4.4' }},
970 WebExternalGecos => { Deprecated => { Instead => 'WebRemoteUserGecos', Remove => '4.4' }},
971 WebExternalAuto => { Deprecated => { Instead => 'WebRemoteUserAutocreate', Remove => '4.4' }},
972 AutoCreate => { Deprecated => { Instead => 'UserAutocreateDefaultsOnLogin', Remove => '4.4' }},
976 Message => "The LogoImageHeight configuration option did not affect display, and has been removed; please remove it from your RT_SiteConfig.pm",
982 Message => "The LogoImageWidth configuration option did not affect display, and has been removed; please remove it from your RT_SiteConfig.pm",
987 my @LOADED_CONFIGS = ();
993 Object constructor returns new object. Takes no arguments.
999 my $class = ref($proto) ? ref($proto) : $proto;
1000 my $self = bless {}, $class;
1011 Load all configs. First of all load RT's config then load
1012 extensions' config files in alphabetical order.
1020 $self->LoadConfig( File => 'RT_Config.pm' );
1022 my @configs = $self->Configs;
1023 $self->LoadConfig( File => $_ ) foreach @configs;
1029 Takes param hash with C<File> field.
1030 First, the site configuration file is loaded, in order to establish
1031 overall site settings like hostname and name of RT instance.
1032 Then, the core configuration file is loaded to set fallback values
1033 for all settings; it bases some values on settings from the site
1036 B<Note> that core config file don't change options if site config
1037 has set them so to add value to some option instead of
1038 overriding you have to copy original value from core config file.
1044 my %args = ( File => '', @_ );
1045 $args{'File'} =~ s/(?<!Site)(?=Config\.pm$)/Site/;
1046 if ( $args{'File'} eq 'RT_SiteConfig.pm'
1047 and my $site_config = $ENV{RT_SITE_CONFIG} )
1049 $self->_LoadConfig( %args, File => $site_config );
1050 # to allow load siteconfig again and again in case it's updated
1051 delete $INC{ $site_config };
1053 $self->_LoadConfig(%args);
1054 delete $INC{$args{'File'}};
1057 $args{'File'} =~ s/Site(?=Config\.pm$)//;
1058 $self->_LoadConfig(%args);
1064 my %args = ( File => '', @_ );
1066 my ($is_ext, $is_site);
1067 if ( $args{'File'} eq ($ENV{RT_SITE_CONFIG}||'') ) {
1068 ($is_ext, $is_site) = ('', 1);
1070 $is_ext = $args{'File'} =~ /^(?!RT_)(?:(.*)_)(?:Site)?Config/ ? $1 : '';
1071 $is_site = $args{'File'} =~ /SiteConfig/ ? 1 : 0;
1076 local *Set = sub(\[$@%]@) {
1077 my ( $opt_ref, @args ) = @_;
1078 my ( $pack, $file, $line ) = caller;
1079 return $self->SetFromConfig(
1085 SiteConfig => $is_site,
1086 Extension => $is_ext,
1089 local *Plugin = sub {
1090 my (@new_plugins) = @_;
1091 my ( $pack, $file, $line ) = caller;
1092 return $self->SetFromConfig(
1093 Option => \@RT::Plugins,
1094 Value => [@RT::Plugins, @new_plugins],
1098 SiteConfig => $is_site,
1099 Extension => $is_ext,
1102 my @etc_dirs = ($RT::LocalEtcPath);
1103 push @etc_dirs, RT->PluginDirs('etc') if $is_ext;
1104 push @etc_dirs, $RT::EtcPath, @INC;
1105 local @INC = @etc_dirs;
1106 require $args{'File'};
1109 return 1 if $is_site && $@ =~ /^Can't locate \Q$args{File}/;
1110 if ( $is_site || $@ !~ /^Can't locate \Q$args{File}/ ) {
1111 die qq{Couldn't load RT config file $args{'File'}:\n\n$@};
1114 my $username = getpwuid($>);
1115 my $group = getgrgid($();
1117 my ( $file_path, $fileuid, $filegid );
1118 foreach ( $RT::LocalEtcPath, $RT::EtcPath, @INC ) {
1119 my $tmp = File::Spec->catfile( $_, $args{File} );
1120 ( $fileuid, $filegid ) = ( stat($tmp) )[ 4, 5 ];
1121 if ( defined $fileuid ) {
1126 unless ($file_path) {
1128 qq{Couldn't load RT config file $args{'File'} as user $username / group $group.\n}
1129 . qq{The file couldn't be found in $RT::LocalEtcPath and $RT::EtcPath.\n$@};
1132 my $message = <<EOF;
1134 RT couldn't load RT config file %s as:
1138 The file is owned by user %s and group %s.
1140 This usually means that the user/group your webserver is running
1141 as cannot read the file. Be careful not to make the permissions
1142 on this file too liberal, because it contains database passwords.
1143 You may need to put the webserver user in the appropriate group
1144 (%s) or change permissions be able to run succesfully.
1147 my $fileusername = getpwuid($fileuid);
1148 my $filegroup = getgrgid($filegid);
1149 my $errormessage = sprintf( $message,
1150 $file_path, $fileusername, $filegroup, $filegroup );
1151 die "$errormessage\n$@";
1153 # Loaded successfully
1154 push @LOADED_CONFIGS, {
1155 as => $args{'File'},
1156 filename => $INC{ $args{'File'} },
1157 extension => $is_ext,
1166 foreach my $o ( grep $META{$_}{'PostLoadCheck'}, $self->Options( Overridable => undef ) ) {
1167 $META{$o}->{'PostLoadCheck'}->( $self, $self->Get($o) );
1173 Returns list of config files found in local etc, plugins' etc
1174 and main etc directories.
1182 foreach my $path ( $RT::LocalEtcPath, RT->PluginDirs('etc'), $RT::EtcPath ) {
1183 my $mask = File::Spec->catfile( $path, "*_Config.pm" );
1184 my @files = glob $mask;
1185 @files = grep !/^RT_Config\.pm$/,
1186 grep $_ && /^\w+_Config\.pm$/,
1187 map { s/^.*[\\\/]//; $_ } @files;
1188 push @configs, sort @files;
1192 @configs = grep !$seen{$_}++, @configs;
1196 =head2 LoadedConfigs
1198 Returns a list of hashrefs, one for each config file loaded. The keys of the
1205 Name this config file was loaded as (relative filename usually).
1209 The full path and filename.
1213 The "extension" part of the filename. For example, the file C<RTIR_Config.pm>
1214 will have an C<extension> value of C<RTIR>.
1218 True if the file is considered a site-level override. For example, C<site>
1219 will be false for C<RT_Config.pm> and true for C<RT_SiteConfig.pm>.
1226 # Copy to avoid the caller changing our internal data
1227 return map { \%$_ } @LOADED_CONFIGS
1232 Takes name of the option as argument and returns its current value.
1234 In the case of a user-overridable option, first checks the user's
1235 preferences before looking for site-wide configuration.
1237 Returns values from RT_SiteConfig, RT_Config and then the %META hash
1238 of configuration variables's "Default" for this config variable,
1241 Returns different things in scalar and array contexts. For scalar
1242 options it's not that important, however for arrays and hash it's.
1243 In scalar context returns references to arrays and hashes.
1245 Use C<scalar> perl's op to force context, especially when you use
1246 C<(..., Argument => RT->Config->Get('ArrayOpt'), ...)>
1247 as perl's '=>' op doesn't change context of the right hand argument to
1248 scalar. Instead use C<(..., Argument => scalar RT->Config->Get('ArrayOpt'), ...)>.
1250 It's also important for options that have no default value(no default
1251 in F<etc/RT_Config.pm>). If you don't force scalar context then you'll
1252 get empty list and all your named args will be messed up. For example
1253 C<(arg1 => 1, arg2 => RT->Config->Get('OptionDoesNotExist'), arg3 => 3)>
1254 will result in C<(arg1 => 1, arg2 => 'arg3', 3)> what is most probably
1255 unexpected, or C<(arg1 => 1, arg2 => RT->Config->Get('ArrayOption'), arg3 => 3)>
1256 will result in C<(arg1 => 1, arg2 => 'element of option', 'another_one' => ..., 'arg3', 3)>.
1261 my ( $self, $name, $user ) = @_;
1264 if ( $user && $user->id && $META{$name}->{'Overridable'} ) {
1265 my $prefs = $user->Preferences($RT::System);
1266 $res = $prefs->{$name} if $prefs;
1268 $res = $OPTIONS{$name} unless defined $res;
1269 $res = $META{$name}->{'Default'} unless defined $res;
1270 return $self->_ReturnValue( $res, $META{$name}->{'Type'} || 'SCALAR' );
1273 =head2 GetObfuscated
1275 the same as Get, except it returns Obfuscated value via Obfuscate sub
1281 my ( $name, $user ) = @_;
1282 my $obfuscate = $META{$name}->{Obfuscate};
1284 # we use two Get here is to simplify the logic of the return value
1285 # configs need obfuscation are supposed to be less, so won't be too heavy
1287 return $self->Get(@_) unless $obfuscate;
1289 my $res = $self->Get(@_);
1290 $res = $obfuscate->( $self, $res, $user );
1291 return $self->_ReturnValue( $res, $META{$name}->{'Type'} || 'SCALAR' );
1296 Set option's value to new value. Takes name of the option and new value.
1299 The new value should be scalar, array or hash depending on type of the option.
1300 If the option is not defined in meta or the default RT config then it is of
1306 my ( $self, $name ) = ( shift, shift );
1308 my $old = $OPTIONS{$name};
1309 my $type = $META{$name}->{'Type'} || 'SCALAR';
1310 if ( $type eq 'ARRAY' ) {
1311 $OPTIONS{$name} = [@_];
1312 { no warnings 'once'; no strict 'refs'; @{"RT::$name"} = (@_); }
1313 } elsif ( $type eq 'HASH' ) {
1314 $OPTIONS{$name} = {@_};
1315 { no warnings 'once'; no strict 'refs'; %{"RT::$name"} = (@_); }
1317 $OPTIONS{$name} = shift;
1318 {no warnings 'once'; no strict 'refs'; ${"RT::$name"} = $OPTIONS{$name}; }
1320 $META{$name}->{'Type'} = $type;
1321 $META{$name}->{'PostSet'}->($self, $OPTIONS{$name}, $old)
1322 if $META{$name}->{'PostSet'};
1323 if ($META{$name}->{'Deprecated'}) {
1324 my %deprecated = %{$META{$name}->{'Deprecated'}};
1325 my $new_var = $deprecated{Instead} || '';
1326 $self->SetFromConfig(
1327 Option => \$new_var,
1328 Value => [$OPTIONS{$name}],
1329 %{$self->Meta($name)->{'Source'}}
1331 $META{$name}->{'PostLoadCheck'} ||= sub {
1333 Message => "Configuration option $name is deprecated",
1339 return $self->_ReturnValue( $old, $type );
1343 my ( $self, $res, $type ) = @_;
1344 return $res unless wantarray;
1346 if ( $type eq 'ARRAY' ) {
1347 return @{ $res || [] };
1348 } elsif ( $type eq 'HASH' ) {
1349 return %{ $res || {} };
1367 unless ( $args{'File'} ) {
1368 ( $args{'Package'}, $args{'File'}, $args{'Line'} ) = caller(1);
1371 my $opt = $args{'Option'};
1374 my $name = Symbol::Global::Name->find($opt);
1380 $type = $META{$name}->{'Type'} || 'SCALAR';
1383 # if option is already set we have to check where
1384 # it comes from and may be ignore it
1385 if ( exists $OPTIONS{$name} ) {
1386 if ( $type eq 'HASH' ) {
1388 @{ $args{'Value'} },
1389 @{ $args{'Value'} }%2? (undef) : (),
1390 $self->Get( $name ),
1392 } elsif ( $args{'SiteConfig'} && $args{'Extension'} ) {
1393 # if it's site config of an extension then it can only
1394 # override options that came from its main config
1395 if ( $args{'Extension'} ne $META{$name}->{'Source'}{'Extension'} ) {
1396 my %source = %{ $META{$name}->{'Source'} };
1398 "Change of config option '$name' at $args{'File'} line $args{'Line'} has been ignored."
1399 ." This option earlier has been set in $source{'File'} line $source{'Line'}."
1400 ." To overide this option use ". ($source{'Extension'}||'RT')
1405 } elsif ( !$args{'SiteConfig'} && $META{$name}->{'Source'}{'SiteConfig'} ) {
1406 # if it's core config then we can override any option that came from another
1407 # core config, but not site config
1409 my %source = %{ $META{$name}->{'Source'} };
1410 if ( $source{'Extension'} ne $args{'Extension'} ) {
1411 # as a site config is loaded earlier then its base config
1412 # then we warn only on different extensions, for example
1413 # RTIR's options is set in main site config
1415 "Change of config option '$name' at $args{'File'} line $args{'Line'} has been ignored."
1416 ." It may be ok, but we want you to be aware."
1417 ." This option has been set earlier in $source{'File'} line $source{'Line'}."
1425 $META{$name}->{'Type'} = $type;
1426 foreach (qw(Package File Line SiteConfig Extension)) {
1427 $META{$name}->{'Source'}->{$_} = $args{$_};
1429 $self->Set( $name, @{ $args{'Value'} } );
1442 return $META{ $_[1] };
1450 map $_->{'Section'} || 'General',
1457 my %args = ( Section => undef, Overridable => 1, Sorted => 1, @_ );
1458 my @res = sort keys %META;
1460 @res = grep( ( $META{$_}->{'Section'} || 'General' ) eq $args{'Section'},
1462 ) if defined $args{'Section'};
1464 if ( defined $args{'Overridable'} ) {
1466 = grep( ( $META{$_}->{'Overridable'} || 0 ) == $args{'Overridable'},
1470 if ( $args{'Sorted'} ) {
1472 ($META{$a}->{SortOrder}||9999) <=> ($META{$b}->{SortOrder}||9999)
1476 @res = sort { $a cmp $b } @res;
1481 =head2 AddOption( Name => '', Section => '', ... )
1492 Widget => '/Widgets/Form/String',
1493 WidgetArguments => {},
1497 unless ( $args{Name} ) {
1498 $RT::Logger->error("Need Name to add a new config");
1502 unless ( $args{Section} ) {
1503 $RT::Logger->error("Need Section to add a new config option");
1507 $META{ delete $args{Name} } = \%args;
1510 =head2 DeleteOption( Name => '' )
1520 if ( $args{Name} ) {
1521 delete $META{$args{Name}};
1524 $RT::Logger->error("Need Name to remove a config option");
1529 =head2 UpdateOption( Name => '' ), Section => '', ... )
1538 Overridable => undef,
1541 WidgetArguments => undef,
1545 my $name = delete $args{Name};
1548 $RT::Logger->error("Need Name to update a new config");
1552 unless ( exists $META{$name} ) {
1553 $RT::Logger->error("Config $name doesn't exist");
1557 for my $type ( keys %args ) {
1558 next unless defined $args{$type};
1559 $META{$name}{$type} = $args{$type};
1564 RT::Base->_ImportOverlays();