# BEGIN BPS TAGGED BLOCK {{{
-#
+#
# COPYRIGHT:
-#
-# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
-# <jesse@bestpractical.com>
-#
+#
+# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
+#
# (Except where explicitly superseded by other copyright notices)
-#
-#
+#
+#
# LICENSE:
-#
+#
# This work is made available to you under the terms of Version 2 of
# the GNU General Public License. A copy of that license should have
# been provided with this software, but in any event can be snarfed
# from www.gnu.org.
-#
+#
# This work is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
-#
+#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301 or visit their web page on the internet at
# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
-#
-#
+#
+#
# CONTRIBUTION SUBMISSION POLICY:
-#
+#
# (The following paragraph is not intended to limit the rights granted
# to you to modify and distribute this software under the terms of
# the GNU General Public License and is only of importance to you if
# you choose to contribute your changes and enhancements to the
# community by submitting them to Best Practical Solutions, LLC.)
-#
+#
# By intentionally submitting any modifications, corrections or
# derivatives to this work, or any other work intended for use with
# Request Tracker, to Best Practical Solutions, LLC, you confirm that
# royalty-free, perpetual, license to use, copy, create derivative
# works based on those contributions, and sublicense and distribute
# those contributions and any derivatives thereof.
-#
+#
# END BPS TAGGED BLOCK }}}
package RT::Config;
use strict;
use warnings;
+use 5.010;
use File::Spec ();
+use Symbol::Global::Name;
+use List::MoreUtils 'uniq';
=head1 NAME
# get config object
use RT::Config;
- my $config = new RT::Config;
+ my $config = RT::Config->new;
$config->LoadConfigs;
# get or set option
can be set for each config optin:
Section - What header this option should be grouped
- under on the user Settings page
+ under on the user Preferences page
Overridable - Can users change this option
SortOrder - Within a Section, how should the options be sorted
for display to the user
Callback - subref that receives no arguments. It returns
a hashref of items that are added to the rest
of the WidgetArguments
+ PostSet - subref passed the RT::Config object and the current and
+ previous setting of the config option. This is called well
+ before much of RT's subsystems are initialized, so what you
+ can do here is pretty limited. It's mostly useful for
+ effecting the value of other config options early.
PostLoadCheck - subref passed the RT::Config object and the current
setting of the config option. Can make further checks
(such as seeing if a library is installed) and then change
the setting of this or other options in the Config using
the RT::Config option.
+ Obfuscate - subref passed the RT::Config object, current setting of the config option
+ and a user object, can return obfuscated value. it's called in
+ RT->Config->GetObfuscated()
=cut
-our %META = (
+our %META;
+%META = (
# General user overridable options
DefaultQueue => {
Section => 'General',
Description => 'Default queue', #loc
Callback => sub {
my $ret = { Values => [], ValuesLabel => {}};
- my $q = new RT::Queues($HTML::Mason::Commands::session{'CurrentUser'});
+ my $q = RT::Queues->new($HTML::Mason::Commands::session{'CurrentUser'});
$q->UnLimit;
while (my $queue = $q->Next) {
next unless $queue->CurrentUserHasRight("CreateTicket");
},
}
},
+ RememberDefaultQueue => {
+ Section => 'General',
+ Overridable => 1,
+ SortOrder => 2,
+ Widget => '/Widgets/Form/Boolean',
+ WidgetArguments => {
+ Description => 'Remember default queue' # loc
+ }
+ },
UsernameFormat => {
Section => 'General',
Overridable => 1,
- SortOrder => 2,
+ SortOrder => 3,
Widget => '/Widgets/Form/Select',
WidgetArguments => {
Description => 'Username format', # loc
- Values => [qw(concise verbose)],
+ Values => [qw(role concise verbose)],
ValuesLabel => {
- concise => 'Short usernames', # loc_left_pair
- verbose => 'Name and email address', # loc_left_pair
+ role => 'Privileged: usernames; Unprivileged: names and email addresses', # loc
+ concise => 'Short usernames', # loc
+ verbose => 'Name and email address', # loc
},
},
},
+ AutocompleteOwners => {
+ Section => 'General',
+ Overridable => 1,
+ SortOrder => 3.1,
+ Widget => '/Widgets/Form/Boolean',
+ WidgetArguments => {
+ Description => 'Use autocomplete to find owners?', # loc
+ Hints => 'Replaces the owner dropdowns with textboxes' #loc
+ }
+ },
WebDefaultStylesheet => {
Section => 'General', #loc
Overridable => 1,
- SortOrder => 3,
+ SortOrder => 4,
Widget => '/Widgets/Form/Select',
WidgetArguments => {
Description => 'Theme', #loc
- # XXX: we need support for 'get values callback'
- Values => [qw(3.5-default 3.4-compat web2 freeside2.1)],
+ Callback => sub {
+ state @stylesheets;
+ unless (@stylesheets) {
+ for my $static_path ( RT::Interface::Web->StaticRoots ) {
+ my $css_path =
+ File::Spec->catdir( $static_path, 'css' );
+ next unless -d $css_path;
+ if ( opendir my $dh, $css_path ) {
+ push @stylesheets, grep {
+ $_ ne 'base' && -e File::Spec->catfile( $css_path, $_, 'main.css' )
+ } readdir $dh;
+ }
+ else {
+ RT->Logger->error("Can't read $css_path: $!");
+ }
+ }
+ @stylesheets = sort { lc $a cmp lc $b } uniq @stylesheets;
+ }
+ return { Values => \@stylesheets };
+ },
+ },
+ PostLoadCheck => sub {
+ my $self = shift;
+ my $value = $self->Get('WebDefaultStylesheet');
+
+ my @roots = RT::Interface::Web->StaticRoots;
+ for my $root (@roots) {
+ return if -d "$root/css/$value";
+ }
+
+ $RT::Logger->warning(
+ "The default stylesheet ($value) does not exist in this instance of RT. "
+ . "Defaulting to freeside4."
+ );
+
+ $self->Set('WebDefaultStylesheet', 'freeside4');
},
},
+ TimeInICal => {
+ Section => 'General',
+ Overridable => 1,
+ SortOrder => 5,
+ Widget => '/Widgets/Form/Boolean',
+ WidgetArguments => {
+ Description => 'Include time in iCal feed events?', # loc
+ Hints => 'Formats iCal feed events with date and time' #loc
+ }
+ },
+ UseSideBySideLayout => {
+ Section => 'Ticket composition',
+ Overridable => 1,
+ SortOrder => 5,
+ Widget => '/Widgets/Form/Boolean',
+ WidgetArguments => {
+ Description => 'Use a two column layout for create and update forms?' # loc
+ }
+ },
MessageBoxRichText => {
- Section => 'General',
+ Section => 'Ticket composition',
Overridable => 1,
- SortOrder => 4,
+ SortOrder => 5.1,
Widget => '/Widgets/Form/Boolean',
WidgetArguments => {
Description => 'WYSIWYG message composer' # loc
}
},
MessageBoxRichTextHeight => {
- Section => 'General',
+ Section => 'Ticket composition',
Overridable => 1,
- SortOrder => 5,
+ SortOrder => 6,
Widget => '/Widgets/Form/Integer',
WidgetArguments => {
Description => 'WYSIWYG composer height', # loc
}
},
MessageBoxWidth => {
- Section => 'General',
+ Section => 'Ticket composition',
Overridable => 1,
- SortOrder => 6,
+ SortOrder => 7,
Widget => '/Widgets/Form/Integer',
WidgetArguments => {
Description => 'Message box width', #loc
},
},
MessageBoxHeight => {
- Section => 'General',
+ Section => 'Ticket composition',
Overridable => 1,
- SortOrder => 7,
+ SortOrder => 8,
Widget => '/Widgets/Form/Integer',
WidgetArguments => {
Description => 'Message box height', #loc
},
},
+ DefaultTimeUnitsToHours => {
+ Section => 'Ticket composition', #loc
+ Overridable => 1,
+ SortOrder => 9,
+ Widget => '/Widgets/Form/Boolean',
+ WidgetArguments => {
+ Description => 'Enter time in hours by default', #loc
+ Hints => 'Only for entry, not display', #loc
+ },
+ },
SearchResultsRefreshInterval => {
Section => 'General', #loc
Overridable => 1,
- SortOrder => 8,
+ SortOrder => 9,
Widget => '/Widgets/Form/Select',
WidgetArguments => {
Description => 'Search results refresh interval', #loc
},
# User overridable options for RT at a glance
- DefaultSummaryRows => {
- Section => 'RT at a glance', #loc
- Overridable => 1,
- SortOrder => 1,
- Widget => '/Widgets/Form/Integer',
- WidgetArguments => {
- Description => 'Number of search results', #loc
- },
- },
HomePageRefreshInterval => {
Section => 'RT at a glance', #loc
Overridable => 1,
},
# User overridable options for Ticket displays
+ PreferRichText => {
+ Section => 'Ticket display', # loc
+ Overridable => 1,
+ SortOrder => 0.9,
+ Widget => '/Widgets/Form/Boolean',
+ WidgetArguments => {
+ Description => 'Display messages in rich text if available', # loc
+ Hints => 'Rich text (HTML) shows formatting such as colored text, bold, italics, and more', # loc
+ },
+ },
MaxInlineBody => {
Section => 'Ticket display', #loc
Overridable => 1,
Description => 'Show oldest history first', #loc
},
},
- ShowUnreadMessageNotifications => {
+ ShowHistory => {
Section => 'Ticket display',
Overridable => 1,
SortOrder => 3,
+ Widget => '/Widgets/Form/Select',
+ WidgetArguments => {
+ Description => 'Show history', #loc
+ Values => [qw(delay click always)],
+ ValuesLabel => {
+ delay => "after the rest of the page loads", #loc
+ click => "after clicking a link", #loc
+ always => "immediately", #loc
+ },
+ },
+ },
+ ShowUnreadMessageNotifications => {
+ Section => 'Ticket display',
+ Overridable => 1,
+ SortOrder => 4,
Widget => '/Widgets/Form/Boolean',
WidgetArguments => {
Description => 'Notify me of unread messages', #loc
},
PlainTextPre => {
+ PostSet => sub {
+ my $self = shift;
+ my $value = shift;
+ $self->SetFromConfig(
+ Option => \'PlainTextMono',
+ Value => [$value],
+ %{$self->Meta('PlainTextPre')->{'Source'}}
+ );
+ },
+ PostLoadCheck => sub {
+ my $self = shift;
+ # XXX: deprecated, remove in 4.4
+ $RT::Logger->info("You set \$PlainTextPre in your config, which has been removed in favor of \$PlainTextMono. Please update your config.")
+ if $self->Meta('PlainTextPre')->{'Source'}{'Package'};
+ },
+ },
+ PlainTextMono => {
Section => 'Ticket display',
Overridable => 1,
- SortOrder => 4,
+ SortOrder => 5,
Widget => '/Widgets/Form/Boolean',
WidgetArguments => {
- Description => 'add <pre> tag around plain text attachments', #loc
- Hints => "Use this to protect the format of plain text" #loc
+ Description => 'Display plain-text attachments in fixed-width font', #loc
+ Hints => 'Display all plain-text attachments in a monospace font with formatting preserved, but wrapping as needed.', #loc
},
},
- PlainTextMono => {
+ MoreAboutRequestorTicketList => {
+ Section => 'Ticket display', #loc
+ Overridable => 1,
+ SortOrder => 6,
+ Widget => '/Widgets/Form/Select',
+ WidgetArguments => {
+ Description => 'What tickets to display in the "More about requestor" box', #loc
+ Values => [qw(Active Inactive All None)],
+ ValuesLabel => {
+ Active => "Show the Requestor's 10 highest priority active tickets", #loc
+ Inactive => "Show the Requestor's 10 highest priority inactive tickets", #loc
+ All => "Show the Requestor's 10 highest priority tickets", #loc
+ None => "Show no tickets for the Requestor", #loc
+ },
+ },
+ },
+ SimplifiedRecipients => {
+ Section => 'Ticket display', #loc
+ Overridable => 1,
+ SortOrder => 7,
+ Widget => '/Widgets/Form/Boolean',
+ WidgetArguments => {
+ Description => "Show simplified recipient list on ticket update", #loc
+ },
+ },
+ DisplayTicketAfterQuickCreate => {
Section => 'Ticket display',
Overridable => 1,
- SortOrder => 5,
+ SortOrder => 8,
Widget => '/Widgets/Form/Boolean',
WidgetArguments => {
- Description => 'display wrapped and formatted plain text attachments', #loc
- Hints => 'Use css rules to display text monospaced and with formatting preserved, but wrap as needed. This does not work well with IE6 and you should use the previous option', #loc
+ Description => 'Display ticket after "Quick Create"', #loc
},
},
+ QuoteFolding => {
+ Section => 'Ticket display',
+ Overridable => 1,
+ SortOrder => 9,
+ Widget => '/Widgets/Form/Boolean',
+ WidgetArguments => {
+ Description => 'Enable quote folding?' # loc
+ }
+ },
# User overridable locale options
DateTimeFormat => {
WidgetArguments => {
Description => 'Date format', #loc
Callback => sub { my $ret = { Values => [], ValuesLabel => {}};
- my $date = new RT::Date($HTML::Mason::Commands::session{'CurrentUser'});
- $date->Set;
+ my $date = RT::Date->new($HTML::Mason::Commands::session{'CurrentUser'});
+ $date->SetToNow;
foreach my $value ($date->Formatters) {
push @{$ret->{Values}}, $value;
- $ret->{ValuesLabel}{$value} = $date->$value();
+ $ret->{ValuesLabel}{$value} = $date->Get(
+ Format => $value,
+ Timezone => 'user',
+ );
}
return $ret;
},
},
},
+ RTAddressRegexp => {
+ Type => 'SCALAR',
+ PostLoadCheck => sub {
+ my $self = shift;
+ my $value = $self->Get('RTAddressRegexp');
+ if (not $value) {
+ $RT::Logger->debug(
+ 'The RTAddressRegexp option is not set in the config.'
+ .' Not setting this option results in additional SQL queries to'
+ .' check whether each address belongs to RT or not.'
+ .' It is especially important to set this option if RT receives'
+ .' emails on addresses that are not in the database or config.'
+ );
+ } elsif (ref $value and ref $value eq "Regexp") {
+ # Ensure that the regex is case-insensitive; while the
+ # local part of email addresses is _technically_
+ # case-sensitive, most MTAs don't treat it as such.
+ $RT::Logger->warning(
+ 'RTAddressRegexp is set to a case-sensitive regular expression.'
+ .' This may lead to mail loops with MTAs which treat the'
+ .' local part as case-insensitive -- which is most of them.'
+ ) if "$value" =~ /^\(\?[a-z]*-([a-z]*):/ and "$1" =~ /i/;
+ }
+ },
+ },
# User overridable mail options
EmailFrequency => {
Section => 'Mail', #loc
]
}
},
+ NotifyActor => {
+ Section => 'Mail', #loc
+ Overridable => 1,
+ SortOrder => 2,
+ Widget => '/Widgets/Form/Boolean',
+ WidgetArguments => {
+ Description => 'Outgoing mail', #loc
+ Hints => 'Should RT send you mail for ticket updates you make?', #loc
+ }
+ },
# this tends to break extensions that stash links in ticket update pages
Organization => {
},
# Internal config options
+ DatabaseExtraDSN => {
+ Type => 'HASH',
+ },
+
+ FullTextSearch => {
+ Type => 'HASH',
+ PostLoadCheck => sub {
+ my $self = shift;
+ my $v = $self->Get('FullTextSearch');
+ return unless $v->{Enable} and $v->{Indexed};
+ my $dbtype = $self->Get('DatabaseType');
+ if ($dbtype eq 'Oracle') {
+ if (not $v->{IndexName}) {
+ $RT::Logger->error("No IndexName set for full-text index; disabling");
+ $v->{Enable} = $v->{Indexed} = 0;
+ }
+ } elsif ($dbtype eq 'Pg') {
+ my $bad = 0;
+ if (not $v->{'Column'}) {
+ $RT::Logger->error("No Column set for full-text index; disabling");
+ $v->{Enable} = $v->{Indexed} = 0;
+ } elsif ($v->{'Column'} eq "Content"
+ and (not $v->{'Table'} or $v->{'Table'} eq "Attachments")) {
+ $RT::Logger->error("Column for full-text index is set to Content, not tsvector column; disabling");
+ $v->{Enable} = $v->{Indexed} = 0;
+ }
+ } elsif ($dbtype eq 'mysql') {
+ if (not $v->{'Table'}) {
+ $RT::Logger->error("No Table set for full-text index; disabling");
+ $v->{Enable} = $v->{Indexed} = 0;
+ } elsif ($v->{'Table'} eq "Attachments") {
+ $RT::Logger->error("Table for full-text index is set to Attachments, not FTS table; disabling");
+ $v->{Enable} = $v->{Indexed} = 0;
+ } else {
+ my (undef, $create) = eval { $RT::Handle->dbh->selectrow_array("SHOW CREATE TABLE " . $v->{Table}); };
+ my ($engine) = ($create||'') =~ /engine=(\S+)/i;
+ if (not $create) {
+ $RT::Logger->error("External table ".$v->{Table}." does not exist");
+ $v->{Enable} = $v->{Indexed} = 0;
+ } elsif (lc $engine eq "sphinx") {
+ # External Sphinx indexer
+ $v->{Sphinx} = 1;
+ unless ($v->{'MaxMatches'}) {
+ $RT::Logger->warn("No MaxMatches set for full-text index; defaulting to 10000");
+ $v->{MaxMatches} = 10_000;
+ }
+ } else {
+ # Internal, one-column table
+ $v->{Column} = 'Content';
+ $v->{Engine} = $engine;
+ }
+ }
+ } else {
+ $RT::Logger->error("Indexed full-text-search not supported for $dbtype");
+ $v->{Indexed} = 0;
+ }
+ },
+ },
DisableGraphViz => {
Type => 'SCALAR',
PostLoadCheck => sub {
my $self = shift;
my $value = shift;
return if $value;
- return if $INC{'GraphViz.pm'};
- local $@;
- return if eval {require GraphViz; 1};
+ return if GraphViz->require;
$RT::Logger->debug("You've enabled GraphViz, but we couldn't load the module: $@");
$self->Set( DisableGraphViz => 1 );
},
my $self = shift;
my $value = shift;
return if $value;
- return if $INC{'GD.pm'};
- local $@;
- return if eval {require GD; 1};
+ return if GD->require;
$RT::Logger->debug("You've enabled GD, but we couldn't load the module: $@");
$self->Set( DisableGD => 1 );
},
},
- MailPlugins => { Type => 'ARRAY' },
- Plugins => { Type => 'ARRAY' },
- GnuPG => { Type => 'HASH' },
- GnuPGOptions => { Type => 'HASH',
+ MailCommand => {
+ Type => 'SCALAR',
+ PostLoadCheck => sub {
+ my $self = shift;
+ my $value = $self->Get('MailCommand');
+ return if ref($value) eq "CODE"
+ or $value =~/^(sendmail|sendmailpipe|qmail|testfile|mbox)$/;
+ $RT::Logger->error("Unknown value for \$MailCommand: $value; defaulting to sendmailpipe");
+ $self->Set( MailCommand => 'sendmailpipe' );
+ },
+ },
+ HTMLFormatter => {
+ Type => 'SCALAR',
+ PostLoadCheck => sub { RT::Interface::Email->_HTMLFormatter },
+ },
+ MailPlugins => {
+ Type => 'ARRAY',
+ PostLoadCheck => sub {
+ my $self = shift;
+
+ # Make sure Crypt is post-loaded first
+ $META{Crypt}{'PostLoadCheck'}->( $self, $self->Get( 'Crypt' ) );
+
+ my @plugins = $self->Get('MailPlugins');
+ if ( grep $_ eq 'Auth::GnuPG' || $_ eq 'Auth::SMIME', @plugins ) {
+ $RT::Logger->warning(
+ 'Auth::GnuPG and Auth::SMIME (from an extension) have been'
+ .' replaced with Auth::Crypt. @MailPlugins has been adjusted,'
+ .' but should be updated to replace both with Auth::Crypt to'
+ .' silence this warning.'
+ );
+ my %seen;
+ @plugins =
+ grep !$seen{$_}++,
+ grep {
+ $_ eq 'Auth::GnuPG' || $_ eq 'Auth::SMIME'
+ ? 'Auth::Crypt' : $_
+ } @plugins;
+ $self->Set( MailPlugins => @plugins );
+ }
+
+ if ( not @{$self->Get('Crypt')->{Incoming}} and grep $_ eq 'Auth::Crypt', @plugins ) {
+ $RT::Logger->warning("Auth::Crypt enabled in MailPlugins, but no available incoming encryption formats");
+ }
+ },
+ },
+ Crypt => {
+ Type => 'HASH',
+ PostLoadCheck => sub {
+ my $self = shift;
+ require RT::Crypt;
+
+ for my $proto (RT::Crypt->EnabledProtocols) {
+ my $opt = $self->Get($proto);
+ if (not RT::Crypt->LoadImplementation($proto)) {
+ $RT::Logger->error("You enabled $proto, but we couldn't load module RT::Crypt::$proto");
+ $opt->{'Enable'} = 0;
+ } elsif (not RT::Crypt->LoadImplementation($proto)->Probe) {
+ $opt->{'Enable'} = 0;
+ } elsif ($META{$proto}{'PostLoadCheck'}) {
+ $META{$proto}{'PostLoadCheck'}->( $self, $self->Get( $proto ) );
+ }
+
+ }
+
+ my $opt = $self->Get('Crypt');
+ my @enabled = RT::Crypt->EnabledProtocols;
+ my %enabled;
+ $enabled{$_} = 1 for @enabled;
+ $opt->{'Enable'} = scalar @enabled;
+ $opt->{'Incoming'} = [ $opt->{'Incoming'} ]
+ if $opt->{'Incoming'} and not ref $opt->{'Incoming'};
+ if ( $opt->{'Incoming'} && @{ $opt->{'Incoming'} } ) {
+ $RT::Logger->warning("$_ explicitly set as incoming Crypt plugin, but not marked Enabled; removing")
+ for grep {not $enabled{$_}} @{$opt->{'Incoming'}};
+ $opt->{'Incoming'} = [ grep {$enabled{$_}} @{$opt->{'Incoming'}} ];
+ } else {
+ $opt->{'Incoming'} = \@enabled;
+ }
+ if ( $opt->{'Outgoing'} ) {
+ if (not $enabled{$opt->{'Outgoing'}}) {
+ $RT::Logger->warning($opt->{'Outgoing'}.
+ " explicitly set as outgoing Crypt plugin, but not marked Enabled; "
+ . (@enabled ? "using $enabled[0]" : "removing"));
+ }
+ $opt->{'Outgoing'} = $enabled[0] unless $enabled{$opt->{'Outgoing'}};
+ } else {
+ $opt->{'Outgoing'} = $enabled[0];
+ }
+ },
+ },
+ SMIME => {
+ Type => 'HASH',
+ PostLoadCheck => sub {
+ my $self = shift;
+ my $opt = $self->Get('SMIME');
+ return unless $opt->{'Enable'};
+
+ if (exists $opt->{Keyring}) {
+ unless ( File::Spec->file_name_is_absolute( $opt->{Keyring} ) ) {
+ $opt->{Keyring} = File::Spec->catfile( $RT::BasePath, $opt->{Keyring} );
+ }
+ unless (-d $opt->{Keyring} and -r _) {
+ $RT::Logger->info(
+ "RT's SMIME libraries couldn't successfully read your".
+ " configured SMIME keyring directory (".$opt->{Keyring}
+ .").");
+ delete $opt->{Keyring};
+ }
+ }
+
+ if (defined $opt->{CAPath}) {
+ if (-d $opt->{CAPath} and -r _) {
+ # directory, all set
+ } elsif (-f $opt->{CAPath} and -r _) {
+ # file, all set
+ } else {
+ $RT::Logger->warn(
+ "RT's SMIME libraries could not read your configured CAPath (".$opt->{CAPath}.")"
+ );
+ delete $opt->{CAPath};
+ }
+ }
+ },
+ },
+ GnuPG => {
+ Type => 'HASH',
PostLoadCheck => sub {
my $self = shift;
my $gpg = $self->Get('GnuPG');
return unless $gpg->{'Enable'};
+
my $gpgopts = $self->Get('GnuPGOptions');
+ unless ( File::Spec->file_name_is_absolute( $gpgopts->{homedir} ) ) {
+ $gpgopts->{homedir} = File::Spec->catfile( $RT::BasePath, $gpgopts->{homedir} );
+ }
unless (-d $gpgopts->{homedir} && -r _ ) { # no homedir, no gpg
- $RT::Logger->debug(
+ $RT::Logger->info(
"RT's GnuPG libraries couldn't successfully read your".
" configured GnuPG home directory (".$gpgopts->{homedir}
- ."). PGP support has been disabled");
+ ."). GnuPG support has been disabled");
$gpg->{'Enable'} = 0;
return;
}
-
- require RT::Crypt::GnuPG;
- unless (RT::Crypt::GnuPG->Probe()) {
- $RT::Logger->debug(
- "RT's GnuPG libraries couldn't successfully execute gpg.".
- " PGP support has been disabled");
- $gpg->{'Enable'} = 0;
+ if ( grep exists $gpg->{$_}, qw(RejectOnMissingPrivateKey RejectOnBadData AllowEncryptDataInDB) ) {
+ $RT::Logger->warning(
+ "The RejectOnMissingPrivateKey, RejectOnBadData and AllowEncryptDataInDB"
+ ." GnuPG options are now properties of the generic Crypt configuration. You"
+ ." should set them there instead."
+ );
+ delete $gpg->{$_} for qw(RejectOnMissingPrivateKey RejectOnBadData AllowEncryptDataInDB);
}
}
},
+ GnuPGOptions => { Type => 'HASH' },
+ ReferrerWhitelist => { Type => 'ARRAY' },
+ WebPath => {
+ PostLoadCheck => sub {
+ my $self = shift;
+ my $value = shift;
+
+ # "In most cases, you should leave $WebPath set to '' (an empty value)."
+ return unless $value;
+
+ # try to catch someone who assumes that you shouldn't leave this empty
+ if ($value eq '/') {
+ $RT::Logger->error("For the WebPath config option, use the empty string instead of /");
+ return;
+ }
+
+ # $WebPath requires a leading / but no trailing /, or it can be blank.
+ return if $value =~ m{^/.+[^/]$};
+
+ if ($value =~ m{/$}) {
+ $RT::Logger->error("The WebPath config option requires no trailing slash");
+ }
+
+ if ($value !~ m{^/}) {
+ $RT::Logger->error("The WebPath config option requires a leading slash");
+ }
+ },
+ },
+ WebDomain => {
+ PostLoadCheck => sub {
+ my $self = shift;
+ my $value = shift;
+
+ if (!$value) {
+ $RT::Logger->error("You must set the WebDomain config option");
+ return;
+ }
+
+ if ($value =~ m{^(\w+://)}) {
+ $RT::Logger->error("The WebDomain config option must not contain a scheme ($1)");
+ return;
+ }
+
+ if ($value =~ m{(/.*)}) {
+ $RT::Logger->error("The WebDomain config option must not contain a path ($1)");
+ return;
+ }
+
+ if ($value =~ m{:(\d*)}) {
+ $RT::Logger->error("The WebDomain config option must not contain a port ($1)");
+ return;
+ }
+ },
+ },
+ WebPort => {
+ PostLoadCheck => sub {
+ my $self = shift;
+ my $value = shift;
+
+ if (!$value) {
+ $RT::Logger->error("You must set the WebPort config option");
+ return;
+ }
+
+ if ($value !~ m{^\d+$}) {
+ $RT::Logger->error("The WebPort config option must be an integer");
+ }
+ },
+ },
+ WebBaseURL => {
+ PostLoadCheck => sub {
+ my $self = shift;
+ my $value = shift;
+
+ if (!$value) {
+ $RT::Logger->error("You must set the WebBaseURL config option");
+ return;
+ }
+
+ if ($value !~ m{^https?://}i) {
+ $RT::Logger->error("The WebBaseURL config option must contain a scheme (http or https)");
+ }
+
+ if ($value =~ m{/$}) {
+ $RT::Logger->error("The WebBaseURL config option requires no trailing slash");
+ }
+
+ if ($value =~ m{^https?://.+?(/[^/].*)}i) {
+ $RT::Logger->error("The WebBaseURL config option must not contain a path ($1)");
+ }
+ },
+ },
+ WebURL => {
+ PostLoadCheck => sub {
+ my $self = shift;
+ my $value = shift;
+
+ if (!$value) {
+ $RT::Logger->error("You must set the WebURL config option");
+ return;
+ }
+
+ if ($value !~ m{^https?://}i) {
+ $RT::Logger->error("The WebURL config option must contain a scheme (http or https)");
+ }
+
+ if ($value !~ m{/$}) {
+ $RT::Logger->error("The WebURL config option requires a trailing slash");
+ }
+ },
+ },
+ EmailInputEncodings => {
+ Type => 'ARRAY',
+ PostLoadCheck => sub {
+ my $self = shift;
+ my $value = $self->Get('EmailInputEncodings');
+ return unless $value && @$value;
+
+ my %seen;
+ foreach my $encoding ( grep defined && length, splice @$value ) {
+ next if $seen{ $encoding };
+ if ( $encoding eq '*' ) {
+ unshift @$value, '*';
+ next;
+ }
+
+ my $canonic = Encode::resolve_alias( $encoding );
+ unless ( $canonic ) {
+ warn "Unknown encoding '$encoding' in \@EmailInputEncodings option";
+ }
+ elsif ( $seen{ $canonic }++ ) {
+ next;
+ }
+ else {
+ push @$value, $canonic;
+ }
+ }
+ },
+ },
+ LogToScreen => {
+ Deprecated => {
+ Instead => 'LogToSTDERR',
+ Remove => '4.4',
+ },
+ },
+ UserAutocompleteFields => {
+ Deprecated => {
+ Instead => 'UserSearchFields',
+ Remove => '4.4',
+ },
+ },
+ CustomFieldGroupings => {
+ Type => 'HASH',
+ PostLoadCheck => sub {
+ my $config = shift;
+ # use scalar context intentionally to avoid not a hash error
+ my $groups = $config->Get('CustomFieldGroupings') || {};
+
+ unless (ref($groups) eq 'HASH') {
+ RT->Logger->error("Config option \%CustomFieldGroupings is a @{[ref $groups]} not a HASH; ignoring");
+ $groups = {};
+ }
+
+ for my $class (keys %$groups) {
+ my @h;
+ if (ref($groups->{$class}) eq 'HASH') {
+ push @h, $_, $groups->{$class}->{$_}
+ for sort {lc($a) cmp lc($b)} keys %{ $groups->{$class} };
+ } elsif (ref($groups->{$class}) eq 'ARRAY') {
+ @h = @{ $groups->{$class} };
+ } else {
+ RT->Logger->error("Config option \%CustomFieldGroupings{$class} is not a HASH or ARRAY; ignoring");
+ delete $groups->{$class};
+ next;
+ }
+
+ $groups->{$class} = [];
+ while (@h) {
+ my $group = shift @h;
+ my $ref = shift @h;
+ if (ref($ref) eq 'ARRAY') {
+ push @{$groups->{$class}}, $group => $ref;
+ } else {
+ RT->Logger->error("Config option \%CustomFieldGroupings{$class}{$group} is not an ARRAY; ignoring");
+ }
+ }
+ }
+ $config->Set( CustomFieldGroupings => %$groups );
+ },
+ },
+ ChartColors => {
+ Type => 'ARRAY',
+ },
+ WebExternalAuth => { Deprecated => { Instead => 'WebRemoteUserAuth', Remove => '4.4' }},
+ WebExternalAuthContinuous => { Deprecated => { Instead => 'WebRemoteUserContinuous', Remove => '4.4' }},
+ WebFallbackToInternalAuth => { Deprecated => { Instead => 'WebFallbackToRTLogin', Remove => '4.4' }},
+ WebExternalGecos => { Deprecated => { Instead => 'WebRemoteUserGecos', Remove => '4.4' }},
+ WebExternalAuto => { Deprecated => { Instead => 'WebRemoteUserAutocreate', Remove => '4.4' }},
+ AutoCreate => { Deprecated => { Instead => 'UserAutocreateDefaultsOnLogin', Remove => '4.4' }},
+ LogoImageHeight => {
+ Deprecated => {
+ LogLevel => "info",
+ Message => "The LogoImageHeight configuration option did not affect display, and has been removed; please remove it from your RT_SiteConfig.pm",
+ },
+ },
+ LogoImageWidth => {
+ Deprecated => {
+ LogLevel => "info",
+ Message => "The LogoImageWidth configuration option did not affect display, and has been removed; please remove it from your RT_SiteConfig.pm",
+ },
+ },
+ DatabaseRequireSSL => {
+ Deprecated => {
+ Remove => '4.4',
+ LogLevel => "info",
+ 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.",
+ },
+ },
);
my %OPTIONS = ();
+my @LOADED_CONFIGS = ();
=head1 METHODS
return;
}
-=head2 InitConfig
-
-Do nothin right now.
-
-=cut
-
-sub InitConfig {
- my $self = shift;
- my %args = ( File => '', @_ );
- $args{'File'} =~ s/(?<=Config)(?=\.pm$)/Meta/;
- return 1;
-}
-
=head2 LoadConfigs
Load all configs. First of all load RT's config then load
sub LoadConfigs {
my $self = shift;
- $self->InitConfig( File => 'RT_Config.pm' );
$self->LoadConfig( File => 'RT_Config.pm' );
my @configs = $self->Configs;
- $self->InitConfig( File => $_ ) foreach @configs;
$self->LoadConfig( File => $_ ) foreach @configs;
return;
}
and my $site_config = $ENV{RT_SITE_CONFIG} )
{
$self->_LoadConfig( %args, File => $site_config );
+ # to allow load siteconfig again and again in case it's updated
+ delete $INC{ $site_config };
} else {
$self->_LoadConfig(%args);
+ delete $INC{$args{'File'}};
}
+
$args{'File'} =~ s/Site(?=Config\.pm$)//;
$self->_LoadConfig(%args);
return 1;
Extension => $is_ext,
);
};
+ local *Plugin = sub {
+ my (@new_plugins) = @_;
+ @new_plugins = map {s/-/::/g if not /:/; $_} @new_plugins;
+ my ( $pack, $file, $line ) = caller;
+ return $self->SetFromConfig(
+ Option => \@RT::Plugins,
+ Value => [@RT::Plugins, @new_plugins],
+ Package => $pack,
+ File => $file,
+ Line => $line,
+ SiteConfig => $is_site,
+ Extension => $is_ext,
+ );
+ };
my @etc_dirs = ($RT::LocalEtcPath);
push @etc_dirs, RT->PluginDirs('etc') if $is_ext;
push @etc_dirs, $RT::EtcPath, @INC;
require $args{'File'};
};
if ($@) {
- return 1 if $is_site && $@ =~ qr{^Can't locate \Q$args{File}};
- if ( $is_site || $@ !~ qr{^Can't locate \Q$args{File}} ) {
+ return 1 if $is_site && $@ =~ /^Can't locate \Q$args{File}/;
+ if ( $is_site || $@ !~ /^Can't locate \Q$args{File}/ ) {
die qq{Couldn't load RT config file $args{'File'}:\n\n$@};
}
my $errormessage = sprintf( $message,
$file_path, $fileusername, $filegroup, $filegroup );
die "$errormessage\n$@";
+ } else {
+ # Loaded successfully
+ push @LOADED_CONFIGS, {
+ as => $args{'File'},
+ filename => $INC{ $args{'File'} },
+ extension => $is_ext,
+ site => $is_site,
+ };
}
return 1;
}
return @configs;
}
+=head2 LoadedConfigs
+
+Returns a list of hashrefs, one for each config file loaded. The keys of the
+hashes are:
+
+=over 4
+
+=item as
+
+Name this config file was loaded as (relative filename usually).
+
+=item filename
+
+The full path and filename.
+
+=item extension
+
+The "extension" part of the filename. For example, the file C<RTIR_Config.pm>
+will have an C<extension> value of C<RTIR>.
+
+=item site
+
+True if the file is considered a site-level override. For example, C<site>
+will be false for C<RT_Config.pm> and true for C<RT_SiteConfig.pm>.
+
+=back
+
+=cut
+
+sub LoadedConfigs {
+ # Copy to avoid the caller changing our internal data
+ return map { \%$_ } @LOADED_CONFIGS
+}
+
=head2 Get
Takes name of the option as argument and returns its current value.
my $res;
if ( $user && $user->id && $META{$name}->{'Overridable'} ) {
- $user = $user->UserObj if $user->isa('RT::CurrentUser');
my $prefs = $user->Preferences($RT::System);
$res = $prefs->{$name} if $prefs;
}
return $self->_ReturnValue( $res, $META{$name}->{'Type'} || 'SCALAR' );
}
+=head2 GetObfuscated
+
+the same as Get, except it returns Obfuscated value via Obfuscate sub
+
+=cut
+
+sub GetObfuscated {
+ my $self = shift;
+ my ( $name, $user ) = @_;
+ my $obfuscate = $META{$name}->{Obfuscate};
+
+ # we use two Get here is to simplify the logic of the return value
+ # configs need obfuscation are supposed to be less, so won't be too heavy
+
+ return $self->Get(@_) unless $obfuscate;
+
+ my $res = $self->Get(@_);
+ $res = $obfuscate->( $self, $res, $user );
+ return $self->_ReturnValue( $res, $META{$name}->{'Type'} || 'SCALAR' );
+}
+
=head2 Set
Set option's value to new value. Takes name of the option and new value.
{no warnings 'once'; no strict 'refs'; ${"RT::$name"} = $OPTIONS{$name}; }
}
$META{$name}->{'Type'} = $type;
+ $META{$name}->{'PostSet'}->($self, $OPTIONS{$name}, $old)
+ if $META{$name}->{'PostSet'};
+ if ($META{$name}->{'Deprecated'}) {
+ my %deprecated = %{$META{$name}->{'Deprecated'}};
+ my $new_var = $deprecated{Instead} || '';
+ $self->SetFromConfig(
+ Option => \$new_var,
+ Value => [$OPTIONS{$name}],
+ %{$self->Meta($name)->{'Source'}}
+ ) if $new_var;
+ $META{$name}->{'PostLoadCheck'} ||= sub {
+ RT->Deprecated(
+ Message => "Configuration option $name is deprecated",
+ Stack => 0,
+ %deprecated,
+ );
+ };
+ }
return $self->_ReturnValue( $old, $type );
}
my $opt = $args{'Option'};
my $type;
- my $name = $self->__GetNameByRef($opt);
+ my $name = Symbol::Global::Name->find($opt);
if ($name) {
$type = ref $opt;
$name =~ s/.*:://;
# if option is already set we have to check where
# it comes from and may be ignore it
if ( exists $OPTIONS{$name} ) {
- if ( $args{'SiteConfig'} && $args{'Extension'} ) {
+ if ( $type eq 'HASH' ) {
+ $args{'Value'} = [
+ @{ $args{'Value'} },
+ @{ $args{'Value'} }%2? (undef) : (),
+ $self->Get( $name ),
+ ];
+ } elsif ( $args{'SiteConfig'} && $args{'Extension'} ) {
# if it's site config of an extension then it can only
# override options that came from its main config
if ( $args{'Extension'} ne $META{$name}->{'Source'}{'Extension'} ) {
if ( $source{'Extension'} ne $args{'Extension'} ) {
# as a site config is loaded earlier then its base config
# then we warn only on different extensions, for example
- # RTIR's options is set in main site config or RTFM's
+ # RTIR's options is set in main site config
warn
"Change of config option '$name' at $args{'File'} line $args{'Line'} has been ignored."
- ." It's may be ok, but we want you to be aware."
- ." This option earlier has been set in $source{'File'} line $source{'Line'}."
+ ." It may be ok, but we want you to be aware."
+ ." This option has been set earlier in $source{'File'} line $source{'Line'}."
;
}
return 1;
}
-{
- my $last_pack = '';
-
- sub __GetNameByRef {
- my $self = shift;
- my $ref = shift;
- my $pack = shift;
- if ( !$pack && $last_pack ) {
- my $tmp = $self->__GetNameByRef( $ref, $last_pack );
- return $tmp if $tmp;
- }
- $pack ||= 'main::';
- $pack .= '::' unless substr( $pack, -2 ) eq '::';
-
- my %ref_sym = (
- SCALAR => '$',
- ARRAY => '@',
- HASH => '%',
- CODE => '&',
- );
- no strict 'refs';
- my $name = undef;
-
- # scan $pack's nametable(hash)
- foreach my $k ( keys %{$pack} ) {
-
- # hash for main:: has reference on itself
- next if $k eq 'main::';
-
- # if entry has trailing '::' then
- # it is link to other name space
- if ( $k =~ /::$/ ) {
- $name = $self->__GetNameByRef( $ref, $k );
- return $name if $name;
- }
-
- # entry of the table with references to
- # SCALAR, ARRAY... and other types with
- # the same name
- my $entry = ${$pack}{$k};
- next unless $entry;
-
- # get entry for type we are looking for
- # XXX skip references to scalars or other references.
- # Otherwie 5.10 goes boom. may be we should skip any
- # reference
- return if ref($entry) eq 'SCALAR' || ref($entry) eq 'REF';
- my $entry_ref = *{$entry}{ ref($ref) };
- next unless $entry_ref;
-
- # if references are equal then we've found
- if ( $entry_ref == $ref ) {
- $last_pack = $pack;
- return ( $ref_sym{ ref($ref) } || '*' ) . $pack . $k;
- }
- }
- return '';
- }
-}
-
=head2 Metadata
sub Sections {
my $self = shift;
my %seen;
- return sort
+ my @sections = sort
grep !$seen{$_}++,
map $_->{'Section'} || 'General',
values %META;
+ return @sections;
}
sub Options {
my $self = shift;
my %args = ( Section => undef, Overridable => 1, Sorted => 1, @_ );
- my @res = keys %META;
+ my @res = sort keys %META;
@res = grep( ( $META{$_}->{'Section'} || 'General' ) eq $args{'Section'},
@res
return @res;
}
-eval "require RT::Config_Vendor";
-if ($@ && $@ !~ qr{^Can't locate RT/Config_Vendor.pm}) {
- die $@;
-};
+=head2 AddOption( Name => '', Section => '', ... )
+
+=cut
+
+sub AddOption {
+ my $self = shift;
+ my %args = (
+ Name => undef,
+ Section => undef,
+ Overridable => 0,
+ SortOrder => undef,
+ Widget => '/Widgets/Form/String',
+ WidgetArguments => {},
+ @_
+ );
+
+ unless ( $args{Name} ) {
+ $RT::Logger->error("Need Name to add a new config");
+ return;
+ }
+
+ unless ( $args{Section} ) {
+ $RT::Logger->error("Need Section to add a new config option");
+ return;
+ }
+
+ $META{ delete $args{Name} } = \%args;
+}
+
+=head2 DeleteOption( Name => '' )
+
+=cut
+
+sub DeleteOption {
+ my $self = shift;
+ my %args = (
+ Name => undef,
+ @_
+ );
+ if ( $args{Name} ) {
+ delete $META{$args{Name}};
+ }
+ else {
+ $RT::Logger->error("Need Name to remove a config option");
+ return;
+ }
+}
+
+=head2 UpdateOption( Name => '' ), Section => '', ... )
+
+=cut
+
+sub UpdateOption {
+ my $self = shift;
+ my %args = (
+ Name => undef,
+ Section => undef,
+ Overridable => undef,
+ SortOrder => undef,
+ Widget => undef,
+ WidgetArguments => undef,
+ @_
+ );
+
+ my $name = delete $args{Name};
+
+ unless ( $name ) {
+ $RT::Logger->error("Need Name to update a new config");
+ return;
+ }
+
+ unless ( exists $META{$name} ) {
+ $RT::Logger->error("Config $name doesn't exist");
+ return;
+ }
+
+ for my $type ( keys %args ) {
+ next unless defined $args{$type};
+ $META{$name}{$type} = $args{$type};
+ }
+ return 1;
+}
-eval "require RT::Config_Local";
-if ($@ && $@ !~ qr{^Can't locate RT/Config_Local.pm}) {
- die $@;
-};
+RT::Base->_ImportOverlays();
1;