1# BEGIN BPS TAGGED BLOCK {{{ 2# 3# COPYRIGHT: 4# 5# This software is Copyright (c) 1996-2021 Best Practical Solutions, LLC 6# <sales@bestpractical.com> 7# 8# (Except where explicitly superseded by other copyright notices) 9# 10# 11# LICENSE: 12# 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 16# from www.gnu.org. 17# 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. 22# 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. 28# 29# 30# CONTRIBUTION SUBMISSION POLICY: 31# 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.) 37# 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. 46# 47# END BPS TAGGED BLOCK }}} 48 49package RT::Config; 50 51use strict; 52use warnings; 53 54use 5.010; 55use File::Spec (); 56use Symbol::Global::Name; 57use List::MoreUtils 'uniq'; 58use Clone (); 59 60# Store log messages generated before RT::Logger is available 61our @PreInitLoggerMessages; 62 63=head1 NAME 64 65 RT::Config - RT's config 66 67=head1 SYNOPSYS 68 69 # get config object 70 use RT::Config; 71 my $config = RT::Config->new; 72 $config->LoadConfigs; 73 74 # get or set option 75 my $rt_web_path = $config->Get('WebPath'); 76 $config->Set(EmailOutputEncoding => 'latin1'); 77 78 # get config object from RT package 79 use RT; 80 RT->LoadConfig; 81 my $config = RT->Config; 82 83=head1 DESCRIPTION 84 85C<RT::Config> class provide access to RT's and RT extensions' config files. 86 87RT uses two files for site configuring: 88 89First file is F<RT_Config.pm> - core config file. This file is shipped 90with RT distribution and contains default values for all available options. 91B<You should never edit this file.> 92 93Second file is F<RT_SiteConfig.pm> - site config file. You can use it 94to customize your RT instance. In this file you can override any option 95listed in core config file. 96 97You may also split settings into separate files under the 98F<etc/RT_SiteConfig.d/> directory. All files ending in C<.pm> will be parsed, 99in alphabetical order, after F<RT_SiteConfig.pm> is loaded. 100 101RT extensions could also provide their config files. Extensions should 102use F<< <NAME>_Config.pm >> and F<< <NAME>_SiteConfig.pm >> names for 103config files, where <NAME> is extension name. 104 105B<NOTE>: All options from RT's config and extensions' configs are saved 106in one place and thus extension could override RT's options, but it is not 107recommended. 108 109=cut 110 111=head2 %META 112 113Hash of Config options that may be user overridable 114or may require more logic than should live in RT_*Config.pm 115 116Keyed by config name, there are several properties that 117can be set for each config optin: 118 119 Section - What header this option should be grouped 120 under on the user Preferences page 121 Overridable - Can users change this option 122 SortOrder - Within a Section, how should the options be sorted 123 for display to the user 124 Widget - Mason component path to widget that should be used 125 to display this config option 126 WidgetArguments - An argument hash passed to the WIdget 127 Description - Friendly description to show the user 128 Values - Arrayref of options (for select Widget) 129 ValuesLabel - Hashref, key is the Value from the Values 130 list, value is a user friendly description 131 of the value 132 Callback - subref that receives no arguments. It returns 133 a hashref of items that are added to the rest 134 of the WidgetArguments 135 PostSet - subref passed the RT::Config object and the current and 136 previous setting of the config option. This is called well 137 before much of RT's subsystems are initialized, so what you 138 can do here is pretty limited. It's mostly useful for 139 effecting the value of other config options early. 140 PostLoadCheck - subref passed the RT::Config object and the current 141 setting of the config option. Can make further checks 142 (such as seeing if a library is installed) and then change 143 the setting of this or other options in the Config using 144 the RT::Config option. 145 Obfuscate - subref passed the RT::Config object, current setting of the config option 146 and a user object, can return obfuscated value. it's called in 147 RT->Config->GetObfuscated() 148 149=cut 150 151our %META; 152%META = ( 153 # General user overridable options 154 RestrictReferrerLogin => { 155 PostLoadCheck => sub { 156 my $self = shift; 157 if (defined($self->Get('RestrictReferrerLogin'))) { 158 RT::Logger->error("The config option 'RestrictReferrerLogin' is incorrect, and should be 'RestrictLoginReferrer' instead."); 159 } 160 }, 161 }, 162 DefaultQueue => { 163 Section => 'General', 164 Overridable => 1, 165 SortOrder => 1, 166 Widget => '/Widgets/Form/Select', 167 WidgetArguments => { 168 Description => 'Default queue', #loc 169 Default => 1, # allow user to unset it on EditConfig.html 170 Callback => sub { 171 my $ret = { Values => [], ValuesLabel => {}}; 172 my $q = RT::Queues->new($HTML::Mason::Commands::session{'CurrentUser'}); 173 $q->UnLimit; 174 while (my $queue = $q->Next) { 175 next unless $queue->CurrentUserHasRight("CreateTicket"); 176 push @{$ret->{Values}}, $queue->Id; 177 $ret->{ValuesLabel}{$queue->Id} = $queue->Name; 178 } 179 return $ret; 180 }, 181 } 182 }, 183 RememberDefaultQueue => { 184 Section => 'General', 185 Overridable => 1, 186 SortOrder => 2, 187 Widget => '/Widgets/Form/Boolean', 188 WidgetArguments => { 189 Description => 'Remember default queue' # loc 190 } 191 }, 192 UsernameFormat => { 193 Section => 'General', 194 Overridable => 1, 195 SortOrder => 3, 196 Widget => '/Widgets/Form/Select', 197 WidgetArguments => { 198 Description => 'Username format', # loc 199 Values => [qw(role concise verbose)], 200 ValuesLabel => { 201 role => 'Privileged: usernames; Unprivileged: names and email addresses', # loc 202 concise => 'Short usernames', # loc 203 verbose => 'Name and email address', # loc 204 }, 205 }, 206 }, 207 AutocompleteOwners => { 208 Section => 'General', 209 Overridable => 1, 210 SortOrder => 3.1, 211 Widget => '/Widgets/Form/Boolean', 212 WidgetArguments => { 213 Description => 'Use autocomplete to find owners?', # loc 214 Hints => 'Replaces the owner dropdowns with textboxes' #loc 215 } 216 }, 217 AutocompleteQueues => { 218 Section => 'General', 219 Overridable => 1, 220 SortOrder => 3.2, 221 Widget => '/Widgets/Form/Boolean', 222 WidgetArguments => { 223 Description => 'Use autocomplete to find queues?', # loc 224 Hints => 'Replaces the queue dropdowns with textboxes' #loc 225 } 226 }, 227 WebDefaultStylesheet => { 228 Section => 'General', #loc 229 Overridable => 1, 230 SortOrder => 4, 231 Widget => '/Widgets/Form/Select', 232 WidgetArguments => { 233 Description => 'Theme', #loc 234 Callback => sub { 235 state @stylesheets; 236 unless (@stylesheets) { 237 for my $static_path ( RT::Interface::Web->StaticRoots ) { 238 my $css_path = 239 File::Spec->catdir( $static_path, 'css' ); 240 next unless -d $css_path; 241 if ( opendir my $dh, $css_path ) { 242 push @stylesheets, grep { 243 -e File::Spec->catfile( $css_path, $_, 'main.css' ) 244 } readdir $dh; 245 } 246 else { 247 RT->Logger->error("Can't read $css_path: $!"); 248 } 249 } 250 @stylesheets = sort { lc $a cmp lc $b } uniq @stylesheets; 251 } 252 return { Values => \@stylesheets }; 253 }, 254 }, 255 PostLoadCheck => sub { 256 my $self = shift; 257 my $value = $self->Get('WebDefaultStylesheet'); 258 259 my @roots = RT::Interface::Web->StaticRoots; 260 for my $root (@roots) { 261 return if -d "$root/css/$value"; 262 } 263 264 $RT::Logger->warning( 265 "The default stylesheet ($value) does not exist in this instance of RT. " 266 . "Defaulting to elevator-light." 267 ); 268 269 $self->Set('WebDefaultStylesheet', 'elevator-light'); 270 }, 271 }, 272 TimeInICal => { 273 Section => 'General', 274 Overridable => 1, 275 SortOrder => 5, 276 Widget => '/Widgets/Form/Boolean', 277 WidgetArguments => { 278 Description => 'Include time in iCal feed events?', # loc 279 Hints => 'Formats iCal feed events with date and time' #loc 280 } 281 }, 282 UseSideBySideLayout => { 283 Section => 'Ticket composition', 284 Overridable => 1, 285 SortOrder => 5, 286 Widget => '/Widgets/Form/Boolean', 287 WidgetArguments => { 288 Description => 'Use a two column layout for create and update forms?' # loc 289 } 290 }, 291 MessageBoxRichText => { 292 Section => 'Ticket composition', 293 Overridable => 1, 294 SortOrder => 5.1, 295 Widget => '/Widgets/Form/Boolean', 296 WidgetArguments => { 297 Description => 'WYSIWYG message composer' # loc 298 } 299 }, 300 MessageBoxRichTextHeight => { 301 Section => 'Ticket composition', 302 Overridable => 1, 303 SortOrder => 6, 304 Widget => '/Widgets/Form/Integer', 305 WidgetArguments => { 306 Description => 'WYSIWYG composer height', # loc 307 } 308 }, 309 MessageBoxWidth => { 310 Section => 'Ticket composition', 311 Overridable => 1, 312 SortOrder => 7, 313 Widget => '/Widgets/Form/Integer', 314 WidgetArguments => { 315 Description => 'Message box width', #loc 316 }, 317 }, 318 MessageBoxHeight => { 319 Section => 'Ticket composition', 320 Overridable => 1, 321 SortOrder => 8, 322 Widget => '/Widgets/Form/Integer', 323 WidgetArguments => { 324 Description => 'Message box height', #loc 325 }, 326 }, 327 DefaultTimeUnitsToHours => { 328 Section => 'Ticket composition', #loc 329 Overridable => 1, 330 SortOrder => 9, 331 Widget => '/Widgets/Form/Boolean', 332 WidgetArguments => { 333 Description => 'Enter time in hours by default', #loc 334 Hints => 'Only for entry, not display', #loc 335 }, 336 }, 337 SignatureAboveQuote => { 338 Section => 'Ticket composition', #loc 339 Overridable => 1, 340 SortOrder => 10, 341 Widget => '/Widgets/Form/Boolean', 342 WidgetArguments => { 343 Description => 'Place signature above quote', #loc 344 }, 345 }, 346 PreferDropzone => { 347 Section => 'Ticket composition', #loc 348 Overridable => 1, 349 SortOrder => 11, 350 Widget => '/Widgets/Form/Boolean', 351 WidgetArguments => { 352 Description => 'Use dropzone if available', #loc 353 }, 354 }, 355 RefreshIntervals => { 356 Type => 'ARRAY', 357 PostLoadCheck => sub { 358 my $self = shift; 359 my @intervals = $self->Get('RefreshIntervals'); 360 if (grep { $_ == 0 } @intervals) { 361 $RT::Logger->warning("Please do not include a 0 value in RefreshIntervals, as that default is already added for you."); 362 } 363 }, 364 }, 365 SearchResultsRefreshInterval => { 366 Section => 'General', #loc 367 Overridable => 1, 368 SortOrder => 9, 369 Widget => '/Widgets/Form/Select', 370 WidgetArguments => { 371 Description => 'Search results refresh interval', #loc 372 Callback => sub { 373 my @values = RT->Config->Get('RefreshIntervals'); 374 my %labels = ( 375 0 => "Don't refresh search results.", # loc 376 ); 377 378 for my $value (@values) { 379 if ($value % 60 == 0) { 380 $labels{$value} = [ 381 'Refresh search results every [quant,_1,minute,minutes].', #loc 382 $value / 60 383 ]; 384 } 385 else { 386 $labels{$value} = [ 387 'Refresh search results every [quant,_1,second,seconds].', #loc 388 $value 389 ]; 390 } 391 } 392 393 unshift @values, 0; 394 395 return { Values => \@values, ValuesLabel => \%labels }; 396 }, 397 }, 398 }, 399 EnableJSChart => { 400 Section => 'General', #loc 401 Overridable => 1, 402 SortOrder => 10, 403 Widget => '/Widgets/Form/Boolean', 404 WidgetArguments => { 405 Description => 'Use JavaScript to render charts', #loc 406 }, 407 }, 408 JSChartColorScheme => { 409 Section => 'General', #loc 410 Overridable => 1, 411 SortOrder => 11, 412 Widget => '/Widgets/Form/String', 413 WidgetArguments => { 414 Description => 'JavaScript chart color scheme', #loc 415 }, 416 }, 417 418 # User overridable options for RT at a glance 419 HomePageRefreshInterval => { 420 Section => 'RT at a glance', #loc 421 Overridable => 1, 422 SortOrder => 2, 423 Widget => '/Widgets/Form/Select', 424 WidgetArguments => { 425 Description => 'Home page refresh interval', #loc 426 Callback => sub { 427 my @values = RT->Config->Get('RefreshIntervals'); 428 my %labels = ( 429 0 => "Don't refresh home page.", # loc 430 ); 431 432 for my $value (@values) { 433 if ($value % 60 == 0) { 434 $labels{$value} = [ 435 'Refresh home page every [quant,_1,minute,minutes].', #loc 436 $value / 60 437 ]; 438 } 439 else { 440 $labels{$value} = [ 441 'Refresh home page every [quant,_1,second,seconds].', #loc 442 $value 443 ]; 444 } 445 } 446 447 unshift @values, 0; 448 449 return { Values => \@values, ValuesLabel => \%labels }; 450 }, 451 }, 452 }, 453 454 # User overridable options for Ticket displays 455 PreferRichText => { 456 Section => 'Ticket display', # loc 457 Overridable => 1, 458 SortOrder => 0.9, 459 Widget => '/Widgets/Form/Boolean', 460 WidgetArguments => { 461 Description => 'Display messages in rich text if available', # loc 462 Hints => 'Rich text (HTML) shows formatting such as colored text, bold, italics, and more', # loc 463 }, 464 }, 465 MaxInlineBody => { 466 Section => 'Ticket display', #loc 467 Overridable => 1, 468 SortOrder => 1, 469 Widget => '/Widgets/Form/Integer', 470 WidgetArguments => { 471 Description => 'Maximum inline message length', #loc 472 Hints => 473 "Length in characters; Use '0' to show all messages inline, regardless of length" #loc 474 }, 475 }, 476 OldestTransactionsFirst => { 477 Section => 'Ticket display', 478 Overridable => 1, 479 SortOrder => 2, 480 Widget => '/Widgets/Form/Boolean', 481 WidgetArguments => { 482 Description => 'Show oldest history first', #loc 483 }, 484 }, 485 ShowHistory => { 486 Section => 'Ticket display', 487 Overridable => 1, 488 SortOrder => 3, 489 Widget => '/Widgets/Form/Select', 490 WidgetArguments => { 491 Description => 'Show history', #loc 492 Values => [qw(delay click always scroll)], 493 ValuesLabel => { 494 delay => "after the rest of the page loads", #loc 495 click => "after clicking a link", #loc 496 always => "immediately", #loc 497 scroll => "as you scroll", #loc 498 }, 499 }, 500 }, 501 ShowUnreadMessageNotifications => { 502 Section => 'Ticket display', 503 Overridable => 1, 504 SortOrder => 4, 505 Widget => '/Widgets/Form/Boolean', 506 WidgetArguments => { 507 Description => 'Notify me of unread messages', #loc 508 }, 509 510 }, 511 PlainTextMono => { 512 Section => 'Ticket display', 513 Overridable => 1, 514 SortOrder => 5, 515 Widget => '/Widgets/Form/Boolean', 516 WidgetArguments => { 517 Description => 'Display plain-text attachments in fixed-width font', #loc 518 Hints => 'Display all plain-text attachments in a monospace font with formatting preserved, but wrapping as needed.', #loc 519 }, 520 }, 521 MoreAboutRequestorTicketList => { 522 Section => 'Ticket display', #loc 523 Overridable => 1, 524 SortOrder => 6, 525 Widget => '/Widgets/Form/Select', 526 WidgetArguments => { 527 Description => 'What tickets to display in the "More about requestor" box', #loc 528 Values => [qw(Active Inactive All None)], 529 ValuesLabel => { 530 Active => "Show the Requestor's 10 highest priority active tickets", #loc 531 Inactive => "Show the Requestor's 10 highest priority inactive tickets", #loc 532 All => "Show the Requestor's 10 highest priority tickets", #loc 533 None => "Show no tickets for the Requestor", #loc 534 }, 535 }, 536 }, 537 SimplifiedRecipients => { 538 Section => 'Ticket display', #loc 539 Overridable => 1, 540 SortOrder => 7, 541 Widget => '/Widgets/Form/Boolean', 542 WidgetArguments => { 543 Description => "Show simplified recipient list on ticket update", #loc 544 }, 545 }, 546 SquelchedRecipients => { 547 Section => 'Ticket display', #loc 548 Overridable => 1, 549 SortOrder => 8, 550 Widget => '/Widgets/Form/Boolean', 551 WidgetArguments => { 552 Description => "Default to squelching all outgoing email notifications (from web interface) on ticket update", #loc 553 }, 554 }, 555 DisplayTicketAfterQuickCreate => { 556 Section => 'Ticket display', 557 Overridable => 1, 558 SortOrder => 9, 559 Widget => '/Widgets/Form/Boolean', 560 WidgetArguments => { 561 Description => 'Display ticket after "Quick Create"', #loc 562 }, 563 }, 564 QuoteFolding => { 565 Section => 'Ticket display', 566 Overridable => 1, 567 SortOrder => 10, 568 Widget => '/Widgets/Form/Boolean', 569 WidgetArguments => { 570 Description => 'Enable quote folding?' # loc 571 } 572 }, 573 HideUnsetFieldsOnDisplay => { 574 Section => 'Ticket display', 575 Overridable => 1, 576 SortOrder => 11, 577 Widget => '/Widgets/Form/Boolean', 578 WidgetArguments => { 579 Description => 'Hide unset fields?' # loc 580 } 581 }, 582 InlineEdit => { 583 Section => 'Ticket display', 584 Overridable => 1, 585 SortOrder => 12, 586 Widget => '/Widgets/Form/Boolean', 587 WidgetArguments => { 588 Description => 'Enable inline edit?' # loc 589 } 590 }, 591 592 InlineEditPanelBehavior => { 593 Type => 'HASH', 594 PostLoadCheck => sub { 595 my $config = shift; 596 # use scalar context intentionally to avoid not a hash error 597 my $behavior = $config->Get('InlineEditPanelBehavior') || {}; 598 599 unless (ref($behavior) eq 'HASH') { 600 RT->Logger->error("Config option \%InlineEditPanelBehavior is a @{[ref $behavior]} not a HASH; ignoring"); 601 $behavior = {}; 602 } 603 604 my %valid = map { $_ => 1 } qw/link click always hide/; 605 for my $class (keys %$behavior) { 606 if (ref($behavior->{$class}) eq 'HASH') { 607 for my $panel (keys %{ $behavior->{$class} }) { 608 my $value = $behavior->{$class}{$panel}; 609 if (!$valid{$value}) { 610 RT->Logger->error("Config option \%InlineEditPanelBehavior{$class}{$panel}, which is '$value', must be one of: " . (join ', ', map { "'$_'" } sort keys %valid) . "; ignoring"); 611 delete $behavior->{$class}{$panel}; 612 } 613 } 614 } else { 615 RT->Logger->error("Config option \%InlineEditPanelBehavior{$class} is not a HASH; ignoring"); 616 delete $behavior->{$class}; 617 next; 618 } 619 } 620 621 $config->Set( InlineEditPanelBehavior => %$behavior ); 622 }, 623 }, 624 ShowSearchNavigation => { 625 Section => 'Ticket display', 626 Overridable => 1, 627 SortOrder => 13, 628 Widget => '/Widgets/Form/Boolean', 629 WidgetArguments => { 630 Description => 'Show search navigation', # loc 631 Hints => 'Show search navigation links of "First", "Last", "Prev" and "Next"', # loc 632 } 633 }, 634 635 # User overridable locale options 636 DateTimeFormat => { 637 Section => 'Locale', #loc 638 Overridable => 1, 639 Widget => '/Widgets/Form/Select', 640 WidgetArguments => { 641 Description => 'Date format', #loc 642 Callback => sub { my $ret = { Values => [], ValuesLabel => {}}; 643 my $date = RT::Date->new($HTML::Mason::Commands::session{'CurrentUser'}); 644 $date->SetToNow; 645 foreach my $value ($date->Formatters) { 646 push @{$ret->{Values}}, $value; 647 $ret->{ValuesLabel}{$value} = $date->Get( 648 Format => $value, 649 Timezone => 'user', 650 ); 651 } 652 return $ret; 653 }, 654 }, 655 }, 656 657 RTAddressRegexp => { 658 Type => 'SCALAR', 659 Immutable => 1, 660 PostLoadCheck => sub { 661 my $self = shift; 662 my $value = $self->Get('RTAddressRegexp'); 663 if (not $value) { 664 $RT::Logger->debug( 665 'The RTAddressRegexp option is not set in the config.' 666 .' Not setting this option results in additional SQL queries to' 667 .' check whether each address belongs to RT or not.' 668 .' It is especially important to set this option if RT receives' 669 .' emails on addresses that are not in the database or config.' 670 ); 671 } elsif (ref $value and ref $value eq "Regexp") { 672 # Ensure that the regex is case-insensitive; while the 673 # local part of email addresses is _technically_ 674 # case-sensitive, most MTAs don't treat it as such. 675 $RT::Logger->warning( 676 'RTAddressRegexp is set to a case-sensitive regular expression.' 677 .' This may lead to mail loops with MTAs which treat the' 678 .' local part as case-insensitive -- which is most of them.' 679 ) if "$value" =~ /^\(\?[a-z]*-([a-z]*):/ and "$1" =~ /i/; 680 } 681 }, 682 }, 683 # User overridable mail options 684 EmailFrequency => { 685 Section => 'Mail', #loc 686 Overridable => 1, 687 Default => 'Individual messages', 688 Widget => '/Widgets/Form/Select', 689 WidgetArguments => { 690 Description => 'Email delivery', #loc 691 Values => [ 692 'Individual messages', #loc 693 'Daily digest', #loc 694 'Weekly digest', #loc 695 'Suspended' #loc 696 ] 697 } 698 }, 699 NotifyActor => { 700 Section => 'Mail', #loc 701 Overridable => 1, 702 SortOrder => 2, 703 Widget => '/Widgets/Form/Boolean', 704 WidgetArguments => { 705 Description => 'Outgoing mail', #loc 706 Hints => 'Should RT send you mail for ticket updates you make?', #loc 707 } 708 }, 709 710 # this tends to break extensions that stash links in ticket update pages 711 Organization => { 712 Type => 'SCALAR', 713 Immutable => 1, 714 Widget => '/Widgets/Form/String', 715 PostLoadCheck => sub { 716 my ($self,$value) = @_; 717 $RT::Logger->error("your \$Organization setting ($value) appears to contain whitespace. Please fix this.") 718 if $value =~ /\s/;; 719 }, 720 }, 721 722 rtname => { 723 Immutable => 1, 724 Widget => '/Widgets/Form/String', 725 }, 726 727 # Internal config options 728 DatabaseExtraDSN => { 729 Type => 'HASH', 730 Immutable => 1, 731 }, 732 DatabaseAdmin => { 733 Immutable => 1, 734 Widget => '/Widgets/Form/String', 735 }, 736 DatabaseHost => { 737 Immutable => 1, 738 Widget => '/Widgets/Form/String', 739 }, 740 DatabaseName => { 741 Immutable => 1, 742 Widget => '/Widgets/Form/String', 743 }, 744 DatabasePassword => { 745 Immutable => 1, 746 Widget => '/Widgets/Form/String', 747 Obfuscate => sub { 748 my ($config, $sources, $user) = @_; 749 return $user->loc('Password not printed'); 750 }, 751 }, 752 DatabasePort => { 753 Immutable => 1, 754 Widget => '/Widgets/Form/Integer', 755 }, 756 DatabaseRTHost => { 757 Immutable => 1, 758 Widget => '/Widgets/Form/String', 759 }, 760 DatabaseType => { 761 Immutable => 1, 762 Widget => '/Widgets/Form/String', 763 }, 764 DatabaseUser => { 765 Immutable => 1, 766 Widget => '/Widgets/Form/String', 767 }, 768 769 FullTextSearch => { 770 Type => 'HASH', 771 PostLoadCheck => sub { 772 my $self = shift; 773 my $v = $self->Get('FullTextSearch'); 774 return unless $v->{Enable} and $v->{Indexed}; 775 my $dbtype = $self->Get('DatabaseType'); 776 if ($dbtype eq 'Oracle') { 777 if (not $v->{IndexName}) { 778 $RT::Logger->error("No IndexName set for full-text index; disabling"); 779 $v->{Enable} = $v->{Indexed} = 0; 780 } 781 } elsif ($dbtype eq 'Pg') { 782 my $bad = 0; 783 if (not $v->{'Column'}) { 784 $RT::Logger->error("No Column set for full-text index; disabling"); 785 $v->{Enable} = $v->{Indexed} = 0; 786 } elsif ($v->{'Column'} eq "Content" 787 and (not $v->{'Table'} or $v->{'Table'} eq "Attachments")) { 788 $RT::Logger->error("Column for full-text index is set to Content, not tsvector column; disabling"); 789 $v->{Enable} = $v->{Indexed} = 0; 790 } 791 } elsif ($dbtype eq 'mysql') { 792 if (not $v->{'Table'}) { 793 $RT::Logger->error("No Table set for full-text index; disabling"); 794 $v->{Enable} = $v->{Indexed} = 0; 795 } elsif ($v->{'Table'} eq "Attachments") { 796 $RT::Logger->error("Table for full-text index is set to Attachments, not FTS table; disabling"); 797 $v->{Enable} = $v->{Indexed} = 0; 798 } else { 799 my (undef, $create) = eval { $RT::Handle->dbh->selectrow_array("SHOW CREATE TABLE " . $v->{Table}); }; 800 my ($engine) = ($create||'') =~ /engine=(\S+)/i; 801 if (not $create) { 802 $RT::Logger->error("External table ".$v->{Table}." does not exist"); 803 $v->{Enable} = $v->{Indexed} = 0; 804 } elsif (lc $engine eq "sphinx") { 805 # External Sphinx indexer 806 $v->{Sphinx} = 1; 807 unless ($v->{'MaxMatches'}) { 808 $RT::Logger->warn("No MaxMatches set for full-text index; defaulting to 10000"); 809 $v->{MaxMatches} = 10_000; 810 } 811 } else { 812 # Internal, one-column table 813 $v->{Column} = 'Content'; 814 $v->{Engine} = $engine; 815 } 816 } 817 } else { 818 $RT::Logger->error("Indexed full-text-search not supported for $dbtype"); 819 $v->{Indexed} = 0; 820 } 821 }, 822 }, 823 DisableGraphViz => { 824 Type => 'SCALAR', 825 Widget => '/Widgets/Form/Boolean', 826 PostLoadCheck => sub { 827 my $self = shift; 828 my $value = shift; 829 return if $value; 830 return if GraphViz->require; 831 $RT::Logger->debug("You've enabled GraphViz, but we couldn't load the module: $@"); 832 $self->Set( DisableGraphViz => 1 ); 833 }, 834 }, 835 DisableGD => { 836 Type => 'SCALAR', 837 Widget => '/Widgets/Form/Boolean', 838 PostLoadCheck => sub { 839 my $self = shift; 840 my $value = shift; 841 return if $value; 842 return if GD->require; 843 $RT::Logger->debug("You've enabled GD, but we couldn't load the module: $@"); 844 $self->Set( DisableGD => 1 ); 845 }, 846 }, 847 MailCommand => { 848 Type => 'SCALAR', 849 Widget => '/Widgets/Form/String', 850 PostLoadCheck => sub { 851 my $self = shift; 852 my $value = $self->Get('MailCommand'); 853 return if ref($value) eq "CODE" 854 or $value =~/^(sendmail|sendmailpipe|qmail|testfile|mbox)$/; 855 $RT::Logger->error("Unknown value for \$MailCommand: $value; defaulting to sendmailpipe"); 856 $self->Set( MailCommand => 'sendmailpipe' ); 857 }, 858 }, 859 HTMLFormatter => { 860 Type => 'SCALAR', 861 Widget => '/Widgets/Form/String', 862 PostLoadCheck => sub { RT::Interface::Email->_HTMLFormatter }, 863 }, 864 Plugins => { 865 Immutable => 1, 866 }, 867 RecordBaseClass => { 868 Immutable => 1, 869 Widget => '/Widgets/Form/String', 870 }, 871 WebSessionClass => { 872 Immutable => 1, 873 Widget => '/Widgets/Form/String', 874 }, 875 DevelMode => { 876 Immutable => 1, 877 Widget => '/Widgets/Form/Boolean', 878 }, 879 DisallowExecuteCode => { 880 Immutable => 1, 881 Widget => '/Widgets/Form/Boolean', 882 }, 883 MailPlugins => { 884 Type => 'ARRAY', 885 Immutable => 1, 886 PostLoadCheck => sub { 887 my $self = shift; 888 889 # Make sure Crypt is post-loaded first 890 $META{Crypt}{'PostLoadCheck'}->( $self, $self->Get( 'Crypt' ) ); 891 892 RT::Interface::Email::Plugins(Add => ["Authz::Default", "Action::Defaults"]); 893 RT::Interface::Email::Plugins(Add => ["Auth::MailFrom"]) 894 unless RT::Interface::Email::Plugins(Code => 1, Method => "GetCurrentUser"); 895 }, 896 }, 897 Crypt => { 898 Immutable => 1, 899 Invisible => 1, 900 Type => 'HASH', 901 PostLoadCheck => sub { 902 my $self = shift; 903 require RT::Crypt; 904 905 for my $proto (RT::Crypt->EnabledProtocols) { 906 my $opt = $self->Get($proto); 907 if (not RT::Crypt->LoadImplementation($proto)) { 908 $RT::Logger->error("You enabled $proto, but we couldn't load module RT::Crypt::$proto"); 909 $opt->{'Enable'} = 0; 910 } elsif (not RT::Crypt->LoadImplementation($proto)->Probe) { 911 $opt->{'Enable'} = 0; 912 } elsif ($META{$proto}{'PostLoadCheck'}) { 913 $META{$proto}{'PostLoadCheck'}->( $self, $self->Get( $proto ) ); 914 } 915 916 } 917 918 my $opt = $self->Get('Crypt'); 919 my @enabled = RT::Crypt->EnabledProtocols; 920 my %enabled; 921 $enabled{$_} = 1 for @enabled; 922 $opt->{'Enable'} = scalar @enabled; 923 $opt->{'Incoming'} = [ $opt->{'Incoming'} ] 924 if $opt->{'Incoming'} and not ref $opt->{'Incoming'}; 925 if ( $opt->{'Incoming'} && @{ $opt->{'Incoming'} } ) { 926 $RT::Logger->warning("$_ explicitly set as incoming Crypt plugin, but not marked Enabled; removing") 927 for grep {not $enabled{$_}} @{$opt->{'Incoming'}}; 928 $opt->{'Incoming'} = [ grep {$enabled{$_}} @{$opt->{'Incoming'}} ]; 929 } else { 930 $opt->{'Incoming'} = \@enabled; 931 } 932 if ( $opt->{'Outgoing'} ) { 933 if (ref($opt->{'Outgoing'}) eq 'HASH') { 934 # Check each entry in the hash 935 foreach my $q (keys(%{$opt->{'Outgoing'}})) { 936 if (not $enabled{$opt->{'Outgoing'}->{$q}}) { 937 if ($q ne '') { 938 $RT::Logger->warning($opt->{'Outgoing'}->{$q}. 939 " explicitly set as outgoing Crypt plugin for queue $q, but not marked Enabled; " 940 . (@enabled ? "using $enabled[0]" : "removing")); 941 } else { 942 $RT::Logger->warning($opt->{'Outgoing'}->{$q}. 943 " explicitly set as default outgoing Crypt plugin, but not marked Enabled; " 944 . (@enabled ? "using $enabled[0]" : "removing")); 945 } 946 $opt->{'Outgoing'}->{$q} = $enabled[0]; 947 } 948 } 949 # If there's no entry for the default queue, set one 950 if (!$opt->{'Outgoing'}->{''} && scalar(@enabled)) { 951 $RT::Logger->warning("No default outgoing Crypt plugin set; using $enabled[0]"); 952 $opt->{'Outgoing'}->{''} = $enabled[0]; 953 } 954 } else { 955 if (not $enabled{$opt->{'Outgoing'}}) { 956 $RT::Logger->warning($opt->{'Outgoing'}. 957 " explicitly set as outgoing Crypt plugin, but not marked Enabled; " 958 . (@enabled ? "using $enabled[0]" : "removing")); 959 } 960 $opt->{'Outgoing'} = $enabled[0] unless $enabled{$opt->{'Outgoing'}}; 961 } 962 } else { 963 $opt->{'Outgoing'} = $enabled[0]; 964 } 965 }, 966 }, 967 SMIME => { 968 Type => 'HASH', 969 Immutable => 1, 970 Invisible => 1, 971 Obfuscate => sub { 972 my ( $config, $value, $user ) = @_; 973 $value->{Passphrase} = $user->loc('Password not printed'); 974 return $value; 975 }, 976 PostLoadCheck => sub { 977 my $self = shift; 978 my $opt = $self->Get('SMIME'); 979 return unless $opt->{'Enable'}; 980 981 if (exists $opt->{Keyring}) { 982 unless ( File::Spec->file_name_is_absolute( $opt->{Keyring} ) ) { 983 $opt->{Keyring} = File::Spec->catfile( $RT::BasePath, $opt->{Keyring} ); 984 } 985 unless (-d $opt->{Keyring} and -r _) { 986 $RT::Logger->info( 987 "RT's SMIME libraries couldn't successfully read your". 988 " configured SMIME keyring directory (".$opt->{Keyring} 989 .")."); 990 delete $opt->{Keyring}; 991 } 992 } 993 994 if (defined $opt->{CAPath}) { 995 if (-d $opt->{CAPath} and -r _) { 996 # directory, all set 997 } elsif (-f $opt->{CAPath} and -r _) { 998 # file, all set 999 } else { 1000 $RT::Logger->warn( 1001 "RT's SMIME libraries could not read your configured CAPath (".$opt->{CAPath}.")" 1002 ); 1003 delete $opt->{CAPath}; 1004 } 1005 } 1006 1007 if ($opt->{CheckCRL} && ! RT::Crypt::SMIME->SupportsCRLfile) { 1008 $opt->{CheckCRL} = 0; 1009 $RT::Logger->warn( 1010 "Your version of OpenSSL does not support the -CRLfile option; disabling \$SMIME{CheckCRL}" 1011 ); 1012 } 1013 }, 1014 }, 1015 GnuPG => { 1016 Type => 'HASH', 1017 Immutable => 1, 1018 Invisible => 1, 1019 Obfuscate => sub { 1020 my ( $config, $value, $user ) = @_; 1021 $value->{Passphrase} = $user->loc('Password not printed'); 1022 return $value; 1023 }, 1024 PostLoadCheck => sub { 1025 my $self = shift; 1026 my $gpg = $self->Get('GnuPG'); 1027 return unless $gpg->{'Enable'}; 1028 1029 my $gpgopts = $self->Get('GnuPGOptions'); 1030 unless ( File::Spec->file_name_is_absolute( $gpgopts->{homedir} ) ) { 1031 $gpgopts->{homedir} = File::Spec->catfile( $RT::BasePath, $gpgopts->{homedir} ); 1032 } 1033 unless (-d $gpgopts->{homedir} && -r _ ) { # no homedir, no gpg 1034 $RT::Logger->info( 1035 "RT's GnuPG libraries couldn't successfully read your". 1036 " configured GnuPG home directory (".$gpgopts->{homedir} 1037 ."). GnuPG support has been disabled"); 1038 $gpg->{'Enable'} = 0; 1039 return; 1040 } 1041 1042 if ( grep exists $gpg->{$_}, qw(RejectOnMissingPrivateKey RejectOnBadData AllowEncryptDataInDB) ) { 1043 $RT::Logger->warning( 1044 "The RejectOnMissingPrivateKey, RejectOnBadData and AllowEncryptDataInDB" 1045 ." GnuPG options are now properties of the generic Crypt configuration. You" 1046 ." should set them there instead." 1047 ); 1048 delete $gpg->{$_} for qw(RejectOnMissingPrivateKey RejectOnBadData AllowEncryptDataInDB); 1049 } 1050 } 1051 }, 1052 GnuPGOptions => { 1053 Type => 'HASH', 1054 Immutable => 1, 1055 Invisible => 1, 1056 Obfuscate => sub { 1057 my ( $config, $value, $user ) = @_; 1058 $value->{passphrase} = $user->loc('Password not printed'); 1059 return $value; 1060 }, 1061 }, 1062 ReferrerWhitelist => { Type => 'ARRAY' }, 1063 EmailDashboardLanguageOrder => { Type => 'ARRAY' }, 1064 CustomFieldValuesCanonicalizers => { Type => 'ARRAY' }, 1065 WebPath => { 1066 Immutable => 1, 1067 Widget => '/Widgets/Form/String', 1068 PostLoadCheck => sub { 1069 my $self = shift; 1070 my $value = shift; 1071 1072 # "In most cases, you should leave $WebPath set to '' (an empty value)." 1073 return unless $value; 1074 1075 # try to catch someone who assumes that you shouldn't leave this empty 1076 if ($value eq '/') { 1077 $RT::Logger->error("For the WebPath config option, use the empty string instead of /"); 1078 return; 1079 } 1080 1081 # $WebPath requires a leading / but no trailing /, or it can be blank. 1082 return if $value =~ m{^/.+[^/]$}; 1083 1084 if ($value =~ m{/$}) { 1085 $RT::Logger->error("The WebPath config option requires no trailing slash"); 1086 } 1087 1088 if ($value !~ m{^/}) { 1089 $RT::Logger->error("The WebPath config option requires a leading slash"); 1090 } 1091 }, 1092 }, 1093 WebDomain => { 1094 Immutable => 1, 1095 Widget => '/Widgets/Form/String', 1096 PostLoadCheck => sub { 1097 my $self = shift; 1098 my $value = shift; 1099 1100 if (!$value) { 1101 $RT::Logger->error("You must set the WebDomain config option"); 1102 return; 1103 } 1104 1105 if ($value =~ m{^(\w+://)}) { 1106 $RT::Logger->error("The WebDomain config option must not contain a scheme ($1)"); 1107 return; 1108 } 1109 1110 if ($value =~ m{(/.*)}) { 1111 $RT::Logger->error("The WebDomain config option must not contain a path ($1)"); 1112 return; 1113 } 1114 1115 if ($value =~ m{:(\d*)}) { 1116 $RT::Logger->error("The WebDomain config option must not contain a port ($1)"); 1117 return; 1118 } 1119 }, 1120 }, 1121 WebPort => { 1122 Immutable => 1, 1123 Widget => '/Widgets/Form/Integer', 1124 PostLoadCheck => sub { 1125 my $self = shift; 1126 my $value = shift; 1127 1128 if (!$value) { 1129 $RT::Logger->error("You must set the WebPort config option"); 1130 return; 1131 } 1132 1133 if ($value !~ m{^\d+$}) { 1134 $RT::Logger->error("The WebPort config option must be an integer"); 1135 } 1136 }, 1137 }, 1138 WebBaseURL => { 1139 Immutable => 1, 1140 Widget => '/Widgets/Form/String', 1141 PostLoadCheck => sub { 1142 my $self = shift; 1143 my $value = shift; 1144 1145 if (!$value) { 1146 $RT::Logger->error("You must set the WebBaseURL config option"); 1147 return; 1148 } 1149 1150 if ($value !~ m{^https?://}i) { 1151 $RT::Logger->error("The WebBaseURL config option must contain a scheme (http or https)"); 1152 } 1153 1154 if ($value =~ m{/$}) { 1155 $RT::Logger->error("The WebBaseURL config option requires no trailing slash"); 1156 } 1157 1158 if ($value =~ m{^https?://.+?(/[^/].*)}i) { 1159 $RT::Logger->error("The WebBaseURL config option must not contain a path ($1)"); 1160 } 1161 }, 1162 }, 1163 WebURL => { 1164 Immutable => 1, 1165 Widget => '/Widgets/Form/String', 1166 PostLoadCheck => sub { 1167 my $self = shift; 1168 my $value = shift; 1169 1170 if (!$value) { 1171 $RT::Logger->error("You must set the WebURL config option"); 1172 return; 1173 } 1174 1175 if ($value !~ m{^https?://}i) { 1176 $RT::Logger->error("The WebURL config option must contain a scheme (http or https)"); 1177 } 1178 1179 if ($value !~ m{/$}) { 1180 $RT::Logger->error("The WebURL config option requires a trailing slash"); 1181 } 1182 }, 1183 }, 1184 EmailInputEncodings => { 1185 Type => 'ARRAY', 1186 PostLoadCheck => sub { 1187 my $self = shift; 1188 my $value = $self->Get('EmailInputEncodings'); 1189 return unless $value && @$value; 1190 1191 my %seen; 1192 foreach my $encoding ( grep defined && length, splice @$value ) { 1193 next if $seen{ $encoding }; 1194 if ( $encoding eq '*' ) { 1195 unshift @$value, '*'; 1196 next; 1197 } 1198 1199 my $canonic = Encode::resolve_alias( $encoding ); 1200 unless ( $canonic ) { 1201 $RT::Logger->warning("Unknown encoding '$encoding' in \@EmailInputEncodings option"); 1202 } 1203 elsif ( $seen{ $canonic }++ ) { 1204 next; 1205 } 1206 else { 1207 push @$value, $canonic; 1208 } 1209 } 1210 }, 1211 }, 1212 CustomFieldGroupings => { 1213 Type => 'HASH', 1214 PostLoadCheck => sub { 1215 my $config = shift; 1216 # use scalar context intentionally to avoid not a hash error 1217 my $groups = $config->Get('CustomFieldGroupings') || {}; 1218 1219 unless (ref($groups) eq 'HASH') { 1220 RT->Logger->error("Config option \%CustomFieldGroupings is a @{[ref $groups]} not a HASH; ignoring"); 1221 $groups = {}; 1222 } 1223 1224 for my $class (keys %$groups) { 1225 my @h; 1226 if (ref($groups->{$class}) eq 'HASH') { 1227 push @h, $_, $groups->{$class}->{$_} 1228 for sort {lc($a) cmp lc($b)} keys %{ $groups->{$class} }; 1229 } elsif (ref($groups->{$class}) eq 'ARRAY') { 1230 @h = @{ $groups->{$class} }; 1231 } else { 1232 RT->Logger->error("Config option \%CustomFieldGroupings{$class} is not a HASH or ARRAY; ignoring"); 1233 delete $groups->{$class}; 1234 next; 1235 } 1236 1237 $groups->{$class} = []; 1238 while (@h) { 1239 my $group = shift @h; 1240 my $ref = shift @h; 1241 if (ref($ref) eq 'ARRAY') { 1242 push @{$groups->{$class}}, $group => $ref; 1243 } else { 1244 RT->Logger->error("Config option \%CustomFieldGroupings{$class}{$group} is not an ARRAY; ignoring"); 1245 } 1246 } 1247 } 1248 $config->Set( CustomFieldGroupings => %$groups ); 1249 }, 1250 }, 1251 CustomDateRanges => { 1252 Type => 'HASH', 1253 Widget => '/Widgets/Form/CustomDateRanges', 1254 PostLoadCheck => sub { 1255 my $config = shift; 1256 # use scalar context intentionally to avoid not a hash error 1257 my $ranges = $config->Get('CustomDateRanges') || {}; 1258 1259 unless (ref($ranges) eq 'HASH') { 1260 RT->Logger->error("Config option \%CustomDateRanges is a @{[ref $ranges]} not a HASH"); 1261 return; 1262 } 1263 1264 for my $class (keys %$ranges) { 1265 if (ref($ranges->{$class}) eq 'HASH') { 1266 for my $name (keys %{ $ranges->{$class} }) { 1267 my $spec = $ranges->{$class}{$name}; 1268 if (!ref($spec) || ref($spec) eq 'HASH') { 1269 # this will produce error messages if parsing fails 1270 $class->require; 1271 $class->_ParseCustomDateRangeSpec($name, $spec); 1272 } 1273 else { 1274 RT->Logger->error("Config option \%CustomDateRanges{$class}{$name} is not a string or HASH"); 1275 } 1276 } 1277 } else { 1278 RT->Logger->error("Config option \%CustomDateRanges{$class} is not a HASH"); 1279 } 1280 } 1281 1282 my %system_config = %$ranges; 1283 if ( my $db_config = $config->Get('CustomDateRangesUI') ) { 1284 for my $type ( keys %$db_config ) { 1285 for my $name ( keys %{ $db_config->{$type} || {} } ) { 1286 if ( $system_config{$type}{$name} ) { 1287 RT->Logger->warning("$type custom date range $name is defined by config file and db"); 1288 } 1289 else { 1290 $system_config{$name} = $db_config->{$type}{$name}; 1291 } 1292 } 1293 } 1294 } 1295 1296 for my $type ( keys %system_config ) { 1297 my $attributes = RT::Attributes->new( RT->SystemUser ); 1298 $attributes->Limit( FIELD => 'Name', VALUE => 'Pref-CustomDateRanges' ); 1299 $attributes->Limit( FIELD => 'ObjectType', VALUE => 'RT::User' ); 1300 $attributes->OrderBy( FIELD => 'id' ); 1301 1302 while ( my $attribute = $attributes->Next ) { 1303 if ( my $content = $attribute->Content ) { 1304 for my $name ( keys %{ $content->{$type} || {} } ) { 1305 if ( $system_config{$type}{$name} ) { 1306 RT->Logger->warning( "$type custom date range $name is defined by system and user #" 1307 . $attribute->ObjectId ); 1308 } 1309 } 1310 } 1311 } 1312 } 1313 }, 1314 }, 1315 CustomDateRangesUI => { 1316 Type => 'HASH', 1317 Widget => '/Widgets/Form/CustomDateRanges', 1318 }, 1319 ExternalStorage => { 1320 Type => 'HASH', 1321 PostLoadCheck => sub { 1322 my $self = shift; 1323 my %hash = $self->Get('ExternalStorage'); 1324 return unless keys %hash; 1325 1326 require RT::ExternalStorage; 1327 1328 my $backend = RT::ExternalStorage::Backend->new(%hash); 1329 RT->System->ExternalStorage($backend); 1330 }, 1331 }, 1332 ChartColors => { 1333 Type => 'ARRAY', 1334 }, 1335 LogoImageHeight => { 1336 Deprecated => { 1337 LogLevel => "info", 1338 Message => "The LogoImageHeight configuration option did not affect display, and has been removed; please remove it from your RT_SiteConfig.pm", 1339 }, 1340 }, 1341 LogoImageWidth => { 1342 Deprecated => { 1343 LogLevel => "info", 1344 Message => "The LogoImageWidth configuration option did not affect display, and has been removed; please remove it from your RT_SiteConfig.pm", 1345 }, 1346 }, 1347 1348 ExternalAuth => { 1349 Immutable => 1, 1350 Widget => '/Widgets/Form/Boolean', 1351 }, 1352 1353 DisablePasswordForAuthToken => { 1354 Widget => '/Widgets/Form/Boolean', 1355 }, 1356 1357 ExternalSettings => { 1358 Immutable => 1, 1359 Obfuscate => sub { 1360 # Ensure passwords are obfuscated on the System Configuration page 1361 my ($config, $sources, $user) = @_; 1362 my $msg = $user->loc('Password not printed'); 1363 1364 for my $source (values %$sources) { 1365 $source->{pass} = $msg; 1366 } 1367 return $sources; 1368 }, 1369 PostLoadCheck => sub { 1370 my $self = shift; 1371 my $settings = shift || {}; 1372 1373 $self->EnableExternalAuth() if keys %$settings > 0; 1374 1375 my $remove = sub { 1376 my ($service) = @_; 1377 delete $settings->{$service}; 1378 1379 $self->Set( 'ExternalAuthPriority', 1380 [ grep { $_ ne $service } @{ $self->Get('ExternalAuthPriority') || [] } ] ); 1381 1382 $self->Set( 'ExternalInfoPriority', 1383 [ grep { $_ ne $service } @{ $self->Get('ExternalInfoPriority') || [] } ] ); 1384 }; 1385 1386 for my $service (keys %$settings) { 1387 my %conf = %{ $settings->{$service} }; 1388 1389 if ($conf{type} !~ /^(ldap|db|cookie)$/) { 1390 $RT::Logger->error( 1391 "Service '$service' in ExternalInfoPriority is not ldap, db, or cookie; removing." 1392 ); 1393 $remove->($service); 1394 next; 1395 } 1396 1397 next unless $conf{type} eq 'db'; 1398 1399 # Ensure people don't misconfigure DBI auth to point to RT's 1400 # Users table; only check server/hostname/table, as 1401 # user/pass might be different (root, for instance) 1402 no warnings 'uninitialized'; 1403 next unless lc $conf{server} eq lc RT->Config->Get('DatabaseHost') and 1404 lc $conf{database} eq lc RT->Config->Get('DatabaseName') and 1405 lc $conf{table} eq 'users'; 1406 1407 $RT::Logger->error( 1408 "RT::Authen::ExternalAuth should _not_ be configured with a database auth service ". 1409 "that points back to RT's internal Users table. Removing the service '$service'! ". 1410 "Please remove it from your config file." 1411 ); 1412 1413 $remove->($service); 1414 } 1415 $self->Set( 'ExternalSettings', $settings ); 1416 }, 1417 }, 1418 1419 ExternalAuthPriority => { 1420 Immutable => 1, 1421 PostLoadCheck => sub { 1422 my $self = shift; 1423 my @values = @{ shift || [] }; 1424 1425 return unless @values or $self->Get('ExternalSettings'); 1426 1427 if (not @values) { 1428 $RT::Logger->debug("ExternalAuthPriority not defined. Attempting to create based on ExternalSettings"); 1429 $self->Set( 'ExternalAuthPriority', \@values ); 1430 return; 1431 } 1432 my %settings; 1433 if ( $self->Get('ExternalSettings') ){ 1434 %settings = %{ $self->Get('ExternalSettings') }; 1435 } 1436 else{ 1437 $RT::Logger->error("ExternalSettings not defined. ExternalAuth requires the ExternalSettings configuration option to operate properly"); 1438 return; 1439 } 1440 for my $key (grep {not $settings{$_}} @values) { 1441 $RT::Logger->error("Removing '$key' from ExternalAuthPriority, as it is not defined in ExternalSettings"); 1442 } 1443 @values = grep {$settings{$_}} @values; 1444 $self->Set( 'ExternalAuthPriority', \@values ); 1445 }, 1446 }, 1447 1448 ExternalInfoPriority => { 1449 Immutable => 1, 1450 PostLoadCheck => sub { 1451 my $self = shift; 1452 my @values = @{ shift || [] }; 1453 1454 return unless @values or $self->Get('ExternalSettings'); 1455 1456 if (not @values) { 1457 $RT::Logger->debug("ExternalInfoPriority not defined. User information (including user enabled/disabled) cannot be externally-sourced"); 1458 $self->Set( 'ExternalInfoPriority', \@values ); 1459 return; 1460 } 1461 1462 my %settings; 1463 if ( $self->Get('ExternalSettings') ){ 1464 %settings = %{ $self->Get('ExternalSettings') }; 1465 } 1466 else{ 1467 $RT::Logger->error("ExternalSettings not defined. ExternalAuth requires the ExternalSettings configuration option to operate properly"); 1468 return; 1469 } 1470 for my $key (grep {not $settings{$_}} @values) { 1471 $RT::Logger->error("Removing '$key' from ExternalInfoPriority, as it is not defined in ExternalSettings"); 1472 } 1473 @values = grep {$settings{$_}} @values; 1474 1475 for my $key (grep {$settings{$_}{type} eq "cookie"} @values) { 1476 $RT::Logger->error("Removing '$key' from ExternalInfoPriority, as cookie authentication cannot be used as an information source"); 1477 } 1478 @values = grep {$settings{$_}{type} ne "cookie"} @values; 1479 1480 $self->Set( 'ExternalInfoPriority', \@values ); 1481 }, 1482 }, 1483 PriorityAsString => { 1484 Type => 'HASH', 1485 PostLoadCheck => sub { 1486 my $self = shift; 1487 return unless $self->Get('EnablePriorityAsString'); 1488 my $config = $self->Get('PriorityAsString'); 1489 1490 my %map; 1491 1492 for my $name ( keys %$config ) { 1493 if ( my $value = $config->{$name} ) { 1494 my @list; 1495 if ( ref $value eq 'ARRAY' ) { 1496 @list = @$value; 1497 } 1498 elsif ( ref $value eq 'HASH' ) { 1499 @list = %$value; 1500 } 1501 else { 1502 RT->Logger->error("Invalid value for $name in PriorityAsString"); 1503 undef $config->{$name}; 1504 } 1505 1506 while ( my $label = shift @list ) { 1507 my $value = shift @list; 1508 $map{$label} //= $value; 1509 1510 if ( $map{$label} != $value ) { 1511 RT->Logger->debug("Priority $label is inconsistent: $map{$label} VS $value"); 1512 } 1513 } 1514 1515 } 1516 } 1517 1518 unless ( keys %map ) { 1519 RT->Logger->debug("No valid PriorityAsString options"); 1520 $self->Set( 'EnablePriorityAsString', 0 ); 1521 } 1522 }, 1523 }, 1524 ServiceBusinessHours => { 1525 Type => 'HASH', 1526 PostLoadCheck => sub { 1527 my $self = shift; 1528 my $config = $self->Get('ServiceBusinessHours'); 1529 for my $name (keys %$config) { 1530 if ($config->{$name}->{7}) { 1531 RT->Logger->error("Config option \%ServiceBusinessHours '$name' erroneously specifies '$config->{$name}->{7}->{Name}' as day 7; Sunday should be specified as day 0."); 1532 } 1533 } 1534 }, 1535 }, 1536 ServiceAgreements => { 1537 Type => 'HASH', 1538 }, 1539 AssetHideSimpleSearch => { 1540 Widget => '/Widgets/Form/Boolean', 1541 }, 1542 AssetMultipleOwner => { 1543 Widget => '/Widgets/Form/Boolean', 1544 }, 1545 AssetShowSearchResultCount => { 1546 Widget => '/Widgets/Form/Boolean', 1547 }, 1548 AllowUserAutocompleteForUnprivileged => { 1549 Widget => '/Widgets/Form/Boolean', 1550 }, 1551 AlwaysDownloadAttachments => { 1552 Widget => '/Widgets/Form/Boolean', 1553 }, 1554 AmbiguousDayInFuture => { 1555 Widget => '/Widgets/Form/Boolean', 1556 }, 1557 AmbiguousDayInPast => { 1558 Widget => '/Widgets/Form/Boolean', 1559 }, 1560 ApprovalRejectionNotes => { 1561 Widget => '/Widgets/Form/Boolean', 1562 }, 1563 ArticleOnTicketCreate => { 1564 Widget => '/Widgets/Form/Boolean', 1565 }, 1566 AutoCreateNonExternalUsers => { 1567 Widget => '/Widgets/Form/Boolean', 1568 }, 1569 AutocompleteOwnersForSearch => { 1570 Widget => '/Widgets/Form/Boolean', 1571 }, 1572 CanonicalizeRedirectURLs => { 1573 Widget => '/Widgets/Form/Boolean', 1574 }, 1575 CanonicalizeURLsInFeeds => { 1576 Widget => '/Widgets/Form/Boolean', 1577 }, 1578 ChartsTimezonesInDB => { 1579 Widget => '/Widgets/Form/Boolean', 1580 }, 1581 CheckMoreMSMailHeaders => { 1582 Widget => '/Widgets/Form/Boolean', 1583 }, 1584 DateDayBeforeMonth => { 1585 Widget => '/Widgets/Form/Boolean', 1586 }, 1587 DisplayTotalTimeWorked => { 1588 Widget => '/Widgets/Form/Boolean', 1589 }, 1590 DontSearchFileAttachments => { 1591 Widget => '/Widgets/Form/Boolean', 1592 }, 1593 DropLongAttachments => { 1594 Widget => '/Widgets/Form/Boolean', 1595 }, 1596 EditCustomFieldsSingleColumn => { 1597 Widget => '/Widgets/Form/Boolean', 1598 }, 1599 EnableReminders => { 1600 Widget => '/Widgets/Form/Boolean', 1601 }, 1602 EnablePriorityAsString => { 1603 Widget => '/Widgets/Form/Boolean', 1604 }, 1605 ExternalStorageDirectLink => { 1606 Widget => '/Widgets/Form/Boolean', 1607 }, 1608 ForceApprovalsView => { 1609 Widget => '/Widgets/Form/Boolean', 1610 }, 1611 ForwardFromUser => { 1612 Widget => '/Widgets/Form/Boolean', 1613 }, 1614 Framebusting => { 1615 Widget => '/Widgets/Form/Boolean', 1616 }, 1617 HideArticleSearchOnReplyCreate => { 1618 Widget => '/Widgets/Form/Boolean', 1619 }, 1620 HideResolveActionsWithDependencies => { 1621 Widget => '/Widgets/Form/Boolean', 1622 }, 1623 HideTimeFieldsFromUnprivilegedUsers => { 1624 Widget => '/Widgets/Form/Boolean', 1625 }, 1626 LoopsToRTOwner => { 1627 Widget => '/Widgets/Form/Boolean', 1628 }, 1629 MessageBoxIncludeSignature => { 1630 Widget => '/Widgets/Form/Boolean', 1631 }, 1632 MessageBoxIncludeSignatureOnComment => { 1633 Widget => '/Widgets/Form/Boolean', 1634 }, 1635 OnlySearchActiveTicketsInSimpleSearch => { 1636 Widget => '/Widgets/Form/Boolean', 1637 }, 1638 ParseNewMessageForTicketCcs => { 1639 Widget => '/Widgets/Form/Boolean', 1640 }, 1641 PreferDateTimeFormatNatural => { 1642 Widget => '/Widgets/Form/Boolean', 1643 }, 1644 PreviewScripMessages => { 1645 Widget => '/Widgets/Form/Boolean', 1646 }, 1647 RecordOutgoingEmail => { 1648 Widget => '/Widgets/Form/Boolean', 1649 }, 1650 RestrictLoginReferrer => { 1651 Widget => '/Widgets/Form/Boolean', 1652 }, 1653 RestrictReferrer => { 1654 Widget => '/Widgets/Form/Boolean', 1655 }, 1656 SearchResultsAutoRedirect => { 1657 Widget => '/Widgets/Form/Boolean', 1658 }, 1659 SelfServiceUseDashboard => { 1660 Widget => '/Widgets/Form/Boolean', 1661 }, 1662 ShowBccHeader => { 1663 Widget => '/Widgets/Form/Boolean', 1664 }, 1665 ShowEditSystemConfig => { 1666 Immutable => 1, 1667 Widget => '/Widgets/Form/Boolean', 1668 }, 1669 ShowEditLifecycleConfig => { 1670 Immutable => 1, 1671 Widget => '/Widgets/Form/Boolean', 1672 }, 1673 ShowMoreAboutPrivilegedUsers => { 1674 Widget => '/Widgets/Form/Boolean', 1675 }, 1676 ShowRTPortal => { 1677 Widget => '/Widgets/Form/Boolean', 1678 }, 1679 ShowRemoteImages => { 1680 Widget => '/Widgets/Form/Boolean', 1681 }, 1682 ShowTransactionImages => { 1683 Widget => '/Widgets/Form/Boolean', 1684 }, 1685 StoreLoops => { 1686 Widget => '/Widgets/Form/Boolean', 1687 }, 1688 StrictLinkACL => { 1689 Widget => '/Widgets/Form/Boolean', 1690 }, 1691 SuppressInlineTextFiles => { 1692 Widget => '/Widgets/Form/Boolean', 1693 }, 1694 TreatAttachedEmailAsFiles => { 1695 Widget => '/Widgets/Form/Boolean', 1696 }, 1697 TruncateLongAttachments => { 1698 Widget => '/Widgets/Form/Boolean', 1699 }, 1700 TrustHTMLAttachments => { 1701 Widget => '/Widgets/Form/Boolean', 1702 }, 1703 UseFriendlyFromLine => { 1704 Widget => '/Widgets/Form/Boolean', 1705 }, 1706 UseFriendlyToLine => { 1707 Widget => '/Widgets/Form/Boolean', 1708 }, 1709 UseOriginatorHeader => { 1710 Widget => '/Widgets/Form/Boolean', 1711 }, 1712 UseSQLForACLChecks => { 1713 Widget => '/Widgets/Form/Boolean', 1714 }, 1715 UseTransactionBatch => { 1716 Widget => '/Widgets/Form/Boolean', 1717 }, 1718 ValidateUserEmailAddresses => { 1719 Widget => '/Widgets/Form/Boolean', 1720 }, 1721 WebFallbackToRTLogin => { 1722 Widget => '/Widgets/Form/Boolean', 1723 }, 1724 WebFlushDbCacheEveryRequest => { 1725 Widget => '/Widgets/Form/Boolean', 1726 }, 1727 WebHttpOnlyCookies => { 1728 Widget => '/Widgets/Form/Boolean', 1729 }, 1730 WebRemoteUserAuth => { 1731 Widget => '/Widgets/Form/Boolean', 1732 }, 1733 WebRemoteUserAutocreate => { 1734 Widget => '/Widgets/Form/Boolean', 1735 }, 1736 WebRemoteUserContinuous => { 1737 Widget => '/Widgets/Form/Boolean', 1738 }, 1739 WebRemoteUserGecos => { 1740 Widget => '/Widgets/Form/Boolean', 1741 }, 1742 WebSecureCookies => { 1743 Widget => '/Widgets/Form/Boolean', 1744 }, 1745 WikiImplicitLinks => { 1746 Widget => '/Widgets/Form/Boolean', 1747 }, 1748 HideOneTimeSuggestions => { 1749 Widget => '/Widgets/Form/Boolean', 1750 }, 1751 LinkArticlesOnInclude => { 1752 Widget => '/Widgets/Form/Boolean', 1753 }, 1754 SelfServiceCorrespondenceOnly => { 1755 Widget => '/Widgets/Form/Boolean', 1756 }, 1757 SelfServiceDownloadUserData => { 1758 Widget => '/Widgets/Form/Boolean', 1759 }, 1760 SelfServiceShowGroupTickets => { 1761 Widget => '/Widgets/Form/Boolean', 1762 }, 1763 SelfServiceShowArticleSearch => { 1764 Widget => '/Widgets/Form/Boolean', 1765 }, 1766 ShowSearchResultCount => { 1767 Widget => '/Widgets/Form/Boolean', 1768 }, 1769 AllowGroupAutocompleteForUnprivileged => { 1770 Widget => '/Widgets/Form/Boolean', 1771 }, 1772 1773 AttachmentListCount => { 1774 Widget => '/Widgets/Form/Integer', 1775 }, 1776 AutoLogoff => { 1777 Widget => '/Widgets/Form/Integer', 1778 }, 1779 BcryptCost => { 1780 Widget => '/Widgets/Form/Integer', 1781 }, 1782 DefaultSummaryRows => { 1783 Widget => '/Widgets/Form/Integer', 1784 }, 1785 DropdownMenuLimit => { 1786 Widget => '/Widgets/Form/Integer', 1787 }, 1788 ExternalStorageCutoffSize => { 1789 Widget => '/Widgets/Form/Integer', 1790 }, 1791 LogoutRefresh => { 1792 Widget => '/Widgets/Form/Integer', 1793 }, 1794 MaxAttachmentSize => { 1795 Widget => '/Widgets/Form/Integer', 1796 }, 1797 MaxFulltextAttachmentSize => { 1798 Widget => '/Widgets/Form/Integer', 1799 }, 1800 MinimumPasswordLength => { 1801 Widget => '/Widgets/Form/Integer', 1802 }, 1803 MoreAboutRequestorGroupsLimit => { 1804 Widget => '/Widgets/Form/Integer', 1805 }, 1806 TicketsItemMapSize => { 1807 Widget => '/Widgets/Form/Integer', 1808 }, 1809 1810 AssetDefaultSearchResultOrderBy => { 1811 Widget => '/Widgets/Form/String', 1812 }, 1813 CanonicalizeEmailAddressMatch => { 1814 Widget => '/Widgets/Form/String', 1815 }, 1816 CanonicalizeEmailAddressReplace => { 1817 Widget => '/Widgets/Form/String', 1818 }, 1819 CommentAddress => { 1820 Widget => '/Widgets/Form/String', 1821 }, 1822 CorrespondAddress => { 1823 Widget => '/Widgets/Form/String', 1824 }, 1825 DashboardAddress => { 1826 Widget => '/Widgets/Form/String', 1827 }, 1828 DashboardSubject => { 1829 Widget => '/Widgets/Form/String', 1830 }, 1831 DefaultErrorMailPrecedence => { 1832 Widget => '/Widgets/Form/String', 1833 }, 1834 DefaultMailPrecedence => { 1835 Widget => '/Widgets/Form/String', 1836 }, 1837 DefaultSearchResultOrderBy => { 1838 Widget => '/Widgets/Form/String', 1839 }, 1840 EmailOutputEncoding => { 1841 Widget => '/Widgets/Form/String', 1842 }, 1843 FriendlyFromLineFormat => { 1844 Widget => '/Widgets/Form/String', 1845 }, 1846 FriendlyToLineFormat => { 1847 Widget => '/Widgets/Form/String', 1848 }, 1849 LDAPHost => { 1850 Widget => '/Widgets/Form/String', 1851 }, 1852 LDAPUser => { 1853 Widget => '/Widgets/Form/String', 1854 }, 1855 LDAPPassword => { 1856 Widget => '/Widgets/Form/String', 1857 Obfuscate => sub { 1858 my ($config, $sources, $user) = @_; 1859 return $user->loc('Password not printed'); 1860 }, 1861 }, 1862 LDAPBase => { 1863 Widget => '/Widgets/Form/String', 1864 }, 1865 LDAPGroupBase => { 1866 Widget => '/Widgets/Form/String', 1867 }, 1868 LogDir => { 1869 Immutable => 1, 1870 Widget => '/Widgets/Form/String', 1871 }, 1872 LogToFileNamed => { 1873 Immutable => 1, 1874 Widget => '/Widgets/Form/String', 1875 }, 1876 LogoAltText => { 1877 Widget => '/Widgets/Form/String', 1878 }, 1879 LogoLinkURL => { 1880 Widget => '/Widgets/Form/String', 1881 }, 1882 LogoURL => { 1883 Widget => '/Widgets/Form/String', 1884 }, 1885 LogoutURL => { 1886 Widget => '/Widgets/Form/String', 1887 }, 1888 OwnerEmail => { 1889 Widget => '/Widgets/Form/String', 1890 }, 1891 QuoteWrapWidth => { 1892 Widget => '/Widgets/Form/Integer', 1893 }, 1894 RedistributeAutoGeneratedMessages => { 1895 Widget => '/Widgets/Form/String', 1896 }, 1897 RTSupportEmail => { 1898 Widget => '/Widgets/Form/String', 1899 }, 1900 SelfServiceRequestUpdateQueue => { 1901 Widget => '/Widgets/Form/String', 1902 }, 1903 SendmailArguments => { 1904 Widget => '/Widgets/Form/String', 1905 }, 1906 SendmailBounceArguments => { 1907 Widget => '/Widgets/Form/String', 1908 }, 1909 SendmailPath => { 1910 Widget => '/Widgets/Form/String', 1911 }, 1912 SetOutgoingMailFrom => { 1913 Widget => '/Widgets/Form/String', 1914 }, 1915 Timezone => { 1916 Widget => '/Widgets/Form/String', 1917 }, 1918 VERPPrefix => { 1919 Widget => '/Widgets/Form/String', 1920 WidgetArguments => { Hints => 'rt-', }, 1921 }, 1922 VERPDomain => { 1923 Widget => '/Widgets/Form/String', 1924 WidgetArguments => { 1925 Callback => sub { return { Hints => RT->Config->Get( 'Organization') } }, 1926 }, 1927 }, 1928 WebImagesURL => { 1929 Widget => '/Widgets/Form/String', 1930 }, 1931 1932 AssetDefaultSearchResultOrder => { 1933 Widget => '/Widgets/Form/Select', 1934 WidgetArguments => { Values => [qw(ASC DESC)] }, 1935 }, 1936 LogToSyslog => { 1937 Immutable => 1, 1938 Widget => '/Widgets/Form/Select', 1939 WidgetArguments => { Values => [qw(debug info notice warning error critical alert emergency)] }, 1940 }, 1941 LogToSTDERR => { 1942 Immutable => 1, 1943 Widget => '/Widgets/Form/Select', 1944 WidgetArguments => { Values => [qw(debug info notice warning error critical alert emergency)] }, 1945 }, 1946 LogToFile => { 1947 Immutable => 1, 1948 Widget => '/Widgets/Form/Select', 1949 WidgetArguments => { Values => [qw(debug info notice warning error critical alert emergency)] }, 1950 }, 1951 LogStackTraces => { 1952 Immutable => 1, 1953 Widget => '/Widgets/Form/Select', 1954 WidgetArguments => { Values => [qw(debug info notice warning error critical alert emergency)] }, 1955 }, 1956 StatementLog => { 1957 Widget => '/Widgets/Form/Select', 1958 WidgetArguments => { Values => ['', qw(debug info notice warning error critical alert emergency)] }, 1959 }, 1960 1961 DefaultCatalog => { 1962 Widget => '/Widgets/Form/Select', 1963 WidgetArguments => { 1964 Description => 'Default catalog', #loc 1965 Callback => sub { 1966 my $ret = { Values => [], ValuesLabel => {} }; 1967 my $c = RT::Catalogs->new( $HTML::Mason::Commands::session{'CurrentUser'} ); 1968 $c->UnLimit; 1969 while ( my $catalog = $c->Next ) { 1970 next unless $catalog->CurrentUserHasRight("CreateAsset"); 1971 push @{ $ret->{Values} }, $catalog->Id; 1972 $ret->{ValuesLabel}{ $catalog->Id } = $catalog->Name; 1973 } 1974 return $ret; 1975 }, 1976 } 1977 }, 1978 DefaultSearchResultOrder => { 1979 Widget => '/Widgets/Form/Select', 1980 WidgetArguments => { Values => [qw(ASC DESC)] }, 1981 }, 1982 SelfServiceUserPrefs => { 1983 Widget => '/Widgets/Form/Select', 1984 WidgetArguments => { 1985 Values => [qw(edit-prefs view-info edit-prefs-view-info full-edit)], 1986 ValuesLabel => { 1987 'edit-prefs' => 'Edit Locale and change password', # loc 1988 'view-info' => 'View all the info', # loc 1989 'edit-prefs-view-info' => 'View all the info, and edit Locale and change password', # loc 1990 'full-edit' => 'View and update all the info', # loc 1991 }, 1992 }, 1993 }, 1994 AssetDefaultSearchResultFormat => { 1995 Widget => '/Widgets/Form/MultilineString', 1996 }, 1997 AssetSimpleSearchFormat => { 1998 Widget => '/Widgets/Form/MultilineString', 1999 }, 2000 AssetSummaryFormat => { 2001 Widget => '/Widgets/Form/MultilineString', 2002 }, 2003 AssetSummaryRelatedTicketsFormat => { 2004 Widget => '/Widgets/Form/MultilineString', 2005 }, 2006 DefaultSearchResultFormat => { 2007 Widget => '/Widgets/Form/MultilineString', 2008 }, 2009 DefaultSelfServiceSearchResultFormat => { 2010 Widget => '/Widgets/Form/MultilineString', 2011 }, 2012 GroupSearchResultFormat => { 2013 Widget => '/Widgets/Form/MultilineString', 2014 }, 2015 GroupSummaryExtraInfo => { 2016 Widget => '/Widgets/Form/MultilineString', 2017 }, 2018 GroupSummaryTicketListFormat => { 2019 Widget => '/Widgets/Form/MultilineString', 2020 }, 2021 LDAPFilter => { 2022 Widget => '/Widgets/Form/MultilineString', 2023 }, 2024 LDAPGroupFilter => { 2025 Widget => '/Widgets/Form/MultilineString', 2026 }, 2027 MoreAboutRequestorExtraInfo => { 2028 Widget => '/Widgets/Form/MultilineString', 2029 }, 2030 MoreAboutRequestorTicketListFormat => { 2031 Widget => '/Widgets/Form/MultilineString', 2032 }, 2033 UserDataResultFormat => { 2034 Widget => '/Widgets/Form/MultilineString', 2035 }, 2036 UserSearchResultFormat => { 2037 Widget => '/Widgets/Form/MultilineString', 2038 }, 2039 UserSummaryExtraInfo => { 2040 Widget => '/Widgets/Form/MultilineString', 2041 }, 2042 UserSummaryTicketListFormat => { 2043 Widget => '/Widgets/Form/MultilineString', 2044 }, 2045 UserTicketDataResultFormat => { 2046 Widget => '/Widgets/Form/MultilineString', 2047 }, 2048 UserTransactionDataResultFormat => { 2049 Widget => '/Widgets/Form/MultilineString', 2050 }, 2051 LogToSyslogConf => { 2052 Immutable => 1, 2053 }, 2054 ShowMobileSite => { 2055 Widget => '/Widgets/Form/Boolean', 2056 }, 2057 StaticRoots => { 2058 Type => 'ARRAY', 2059 Immutable => 1, 2060 }, 2061 EmailSubjectTagRegex => { 2062 Immutable => 1, 2063 }, 2064 ExtractSubjectTagMatch => { 2065 Immutable => 1, 2066 }, 2067 ExtractSubjectTagNoMatch => { 2068 Immutable => 1, 2069 }, 2070 WebNoAuthRegex => { 2071 Immutable => 1, 2072 }, 2073 SelfServiceRegex => { 2074 Immutable => 1, 2075 }, 2076); 2077my %OPTIONS = (); 2078my @LOADED_CONFIGS = (); 2079 2080=head1 METHODS 2081 2082=head2 new 2083 2084Object constructor returns new object. Takes no arguments. 2085 2086=cut 2087 2088sub new { 2089 my $proto = shift; 2090 my $class = ref($proto) ? ref($proto) : $proto; 2091 my $self = bless {}, $class; 2092 $self->_Init(@_); 2093 return $self; 2094} 2095 2096sub _Init { 2097 return; 2098} 2099 2100=head2 LoadConfigs 2101 2102Load all configs. First of all load RT's config then load 2103extensions' config files in alphabetical order. 2104Takes no arguments. 2105 2106=cut 2107 2108sub LoadConfigs { 2109 my $self = shift; 2110 2111 $self->LoadConfig( File => 'RT_Config.pm' ); 2112 2113 my @configs = $self->Configs; 2114 $self->LoadConfig( File => $_ ) foreach @configs; 2115 return; 2116} 2117 2118=head1 LoadConfig 2119 2120Takes param hash with C<File> field. 2121First, the site configuration file is loaded, in order to establish 2122overall site settings like hostname and name of RT instance. 2123Then, the core configuration file is loaded to set fallback values 2124for all settings; it bases some values on settings from the site 2125configuration file. 2126 2127B<Note> that core config file don't change options if site config 2128has set them so to add value to some option instead of 2129overriding you have to copy original value from core config file. 2130 2131=cut 2132 2133sub LoadConfig { 2134 my $self = shift; 2135 my %args = ( File => '', @_ ); 2136 $args{'File'} =~ s/(?<!Site)(?=Config\.pm$)/Site/; 2137 if ( $args{'File'} eq 'RT_SiteConfig.pm' ) { 2138 my $load = $ENV{RT_SITE_CONFIG} || $args{'File'}; 2139 $self->_LoadConfig( %args, File => $load ); 2140 # to allow load siteconfig again and again in case it's updated 2141 delete $INC{$load}; 2142 2143 my $dir = $ENV{RT_SITE_CONFIG_DIR} || "$RT::EtcPath/RT_SiteConfig.d"; 2144 my $localdir = $ENV{RT_SITE_CONFIG_DIR} || "$RT::LocalEtcPath/RT_SiteConfig.d"; 2145 for my $file ( sort(<$dir/*.pm>), sort(<$localdir/*.pm>) ) { 2146 $self->_LoadConfig( %args, File => $file, Site => 1, Extension => '' ); 2147 delete $INC{$file}; 2148 } 2149 } 2150 else { 2151 $self->_LoadConfig(%args); 2152 delete $INC{$args{'File'}}; 2153 } 2154 2155 $args{'File'} =~ s/Site(?=Config\.pm$)//; 2156 $self->_LoadConfig(%args); 2157 return 1; 2158} 2159 2160sub _LoadConfig { 2161 my $self = shift; 2162 my %args = ( File => '', @_ ); 2163 2164 my ($is_ext, $is_site); 2165 if ( defined $args{Site} && defined $args{Extension} ) { 2166 $is_ext = $args{Extension}; 2167 $is_site = $args{Site}; 2168 } 2169 elsif ( $args{'File'} eq ($ENV{RT_SITE_CONFIG}||'') ) { 2170 ($is_ext, $is_site) = ('', 1); 2171 } else { 2172 $is_ext = $args{'File'} =~ /^(?!RT_)(?:(.*)_)(?:Site)?Config/ ? $1 : ''; 2173 $is_site = $args{'File'} =~ /SiteConfig/ ? 1 : 0; 2174 } 2175 2176 eval { 2177 package RT; 2178 local *Set = sub(\[$@%]@) { 2179 my ( $opt_ref, @args ) = @_; 2180 my ( $pack, $file, $line ) = caller; 2181 return $self->SetFromConfig( 2182 Option => $opt_ref, 2183 Value => [@args], 2184 Package => $pack, 2185 File => $file, 2186 Line => $line, 2187 SiteConfig => $is_site, 2188 Extension => $is_ext, 2189 ); 2190 }; 2191 local *Plugin = sub { 2192 my (@new_plugins) = @_; 2193 @new_plugins = map {s/-/::/g if not /:/; $_} @new_plugins; 2194 my ( $pack, $file, $line ) = caller; 2195 return $self->SetFromConfig( 2196 Option => \@RT::Plugins, 2197 Value => [@RT::Plugins, @new_plugins], 2198 Package => $pack, 2199 File => $file, 2200 Line => $line, 2201 SiteConfig => $is_site, 2202 Extension => $is_ext, 2203 ); 2204 }; 2205 my @etc_dirs = ($RT::LocalEtcPath); 2206 push @etc_dirs, RT->PluginDirs('etc') if $is_ext; 2207 push @etc_dirs, $RT::EtcPath, @INC; 2208 local @INC = @etc_dirs; 2209 eval { require $args{'File'} }; 2210 if ( $@ && $@ !~ /did not return a true value/ ) { 2211 die $@; 2212 } 2213 }; 2214 if ($@) { 2215 2216 if ( $is_site && $@ =~ /^Can't locate \Q$args{File}/ ) { 2217 2218 # Since perl 5.18, the "Can't locate ..." error message contains 2219 # more details. warn to help debug if there is a permission issue. 2220 warn qq{Couldn't load RT config file $args{'File'}:\n\n$@} if $@ =~ /Permission denied at/; 2221 return 1; 2222 } 2223 2224 if ( $is_site || $@ !~ /^Can't locate \Q$args{File}/ ) { 2225 die qq{Couldn't load RT config file $args{'File'}:\n\n$@}; 2226 } 2227 2228 my $username = getpwuid($>); 2229 my $group = getgrgid($(); 2230 2231 my ( $file_path, $fileuid, $filegid ); 2232 foreach ( $RT::LocalEtcPath, $RT::EtcPath, @INC ) { 2233 my $tmp = File::Spec->catfile( $_, $args{File} ); 2234 ( $fileuid, $filegid ) = ( stat($tmp) )[ 4, 5 ]; 2235 if ( defined $fileuid ) { 2236 $file_path = $tmp; 2237 last; 2238 } 2239 } 2240 unless ($file_path) { 2241 die 2242 qq{Couldn't load RT config file $args{'File'} as user $username / group $group.\n} 2243 . qq{The file couldn't be found in $RT::LocalEtcPath and $RT::EtcPath.\n$@}; 2244 } 2245 2246 my $message = <<EOF; 2247 2248RT couldn't load RT config file %s as: 2249 user: $username 2250 group: $group 2251 2252The file is owned by user %s and group %s. 2253 2254This usually means that the user/group your webserver is running 2255as cannot read the file. Be careful not to make the permissions 2256on this file too liberal, because it contains database passwords. 2257You may need to put the webserver user in the appropriate group 2258(%s) or change permissions be able to run succesfully. 2259EOF 2260 2261 my $fileusername = getpwuid($fileuid); 2262 my $filegroup = getgrgid($filegid); 2263 my $errormessage = sprintf( $message, 2264 $file_path, $fileusername, $filegroup, $filegroup ); 2265 die "$errormessage\n$@"; 2266 } else { 2267 # Loaded successfully 2268 push @LOADED_CONFIGS, { 2269 as => $args{'File'}, 2270 filename => $INC{ $args{'File'} }, 2271 extension => $is_ext, 2272 site => $is_site, 2273 }; 2274 } 2275 return 1; 2276} 2277 2278sub PostLoadCheck { 2279 my $self = shift; 2280 foreach my $o ( grep $META{$_}{'PostLoadCheck'}, $self->Options( Overridable => undef ) ) { 2281 $META{$o}->{'PostLoadCheck'}->( $self, $self->Get($o) ); 2282 } 2283} 2284 2285=head2 SectionMap 2286 2287A data structure used to breakup the option list into tabs/sections/subsections/options 2288This is done by parsing RT_Config.pm. 2289 2290=cut 2291 2292our $SectionMap = []; 2293our $SectionMapLoaded = 0; # so we only load it once 2294 2295sub LoadSectionMap { 2296 my $self = shift; 2297 2298 if ($SectionMapLoaded) { 2299 return $SectionMap; 2300 } 2301 2302 my $ConfigFile = "$RT::EtcPath/RT_Config.pm"; 2303 require Pod::Simple::HTML; 2304 my $PodParser = Pod::Simple::HTML->new(); 2305 2306 my $html; 2307 $PodParser->output_string( \$html ); 2308 $PodParser->parse_file($ConfigFile); 2309 2310 my $has_subsection; 2311 while ( $html =~ m{<(h[123]|dt)\b[^>]*>(.*?)</\1>}sg ) { 2312 my ( $tag, $content ) = ( $1, $2 ); 2313 if ( $tag eq 'h1' ) { 2314 my ($title) = $content =~ m{<a class='u'\s*name="[^"]*"\s*>([^<]*)</a>}; 2315 next if $title =~ /^(?:NAME|DESCRIPTION)$/; 2316 push @$SectionMap, { Name => $title, Content => [] }; 2317 } 2318 elsif (@$SectionMap) { 2319 if ( $tag eq 'h2' ) { 2320 my ($title) = $content =~ m{<a class='u'\s*name="[^"]*"\s*>([^<]*)</a>}; 2321 push @{ $SectionMap->[-1]{Content} }, { Name => $title, Content => [] }; 2322 $has_subsection = 0; 2323 } 2324 elsif ( $tag eq 'h3' ) { 2325 my ($title) = $content =~ m{<a class='u'\s*name="[^"]*"\s*>([^<]*)</a>}; 2326 push @{ $SectionMap->[-1]{Content}[-1]{Content} }, { Name => $title, Content => [] }; 2327 $has_subsection ||= 1; 2328 } 2329 else { 2330 # tag is 'dt' 2331 if ( !$has_subsection ) { 2332 2333 # Create an empty subsection to keep the same data structure 2334 push @{ $SectionMap->[-1]{Content}[-1]{Content} }, { Name => '', Content => [] }; 2335 $has_subsection = 1; 2336 } 2337 2338 # a single item (dt) can document several options, in separate <code> elements 2339 my ($name) = $content =~ m{name=".([^"]*)"}; 2340 $name =~ s{,_.}{-}g; # e.g. DatabaseHost,_$DatabaseRTHost 2341 while ( $content =~ m{<code>(.)([^<]*)</code>}sg ) { 2342 my ( $sigil, $option ) = ( $1, $2 ); 2343 next unless $sigil =~ m{[\@\%\$]}; # no sigil => this is a value for a select option 2344 if ( $META{$option} ) { 2345 next if $META{$option}{Invisible}; 2346 } 2347 push @{ $SectionMap->[-1]{Content}[-1]{Content}[-1]{Content} }, { Name => $option, Help => $name }; 2348 } 2349 } 2350 } 2351 } 2352 2353 # Remove empty tabs/sections 2354 for my $tab (@$SectionMap) { 2355 for my $section ( @{ $tab->{Content} } ) { 2356 @{ $section->{Content} } = grep { @{ $_->{Content} } } @{ $section->{Content} }; 2357 } 2358 @{ $tab->{Content} } = grep { @{ $_->{Content} } } @{ $tab->{Content} }; 2359 } 2360 @$SectionMap = grep { @{ $_->{Content} } } @$SectionMap; 2361 2362 $SectionMapLoaded = 1; 2363 return $SectionMap; 2364} 2365 2366=head2 Configs 2367 2368Returns list of config files found in local etc, plugins' etc 2369and main etc directories. 2370 2371=cut 2372 2373sub Configs { 2374 my $self = shift; 2375 2376 my @configs = (); 2377 foreach my $path ( $RT::LocalEtcPath, RT->PluginDirs('etc'), $RT::EtcPath ) { 2378 my $mask = File::Spec->catfile( $path, "*_Config.pm" ); 2379 my @files = glob $mask; 2380 @files = grep !/^RT_Config\.pm$/, 2381 grep $_ && /^\w+_Config\.pm$/, 2382 map { s/^.*[\\\/]//; $_ } @files; 2383 push @configs, sort @files; 2384 } 2385 2386 my %seen; 2387 @configs = grep !$seen{$_}++, @configs; 2388 return @configs; 2389} 2390 2391=head2 LoadedConfigs 2392 2393Returns a list of hashrefs, one for each config file loaded. The keys of the 2394hashes are: 2395 2396=over 4 2397 2398=item as 2399 2400Name this config file was loaded as (relative filename usually). 2401 2402=item filename 2403 2404The full path and filename. 2405 2406=item extension 2407 2408The "extension" part of the filename. For example, the file C<RTIR_Config.pm> 2409will have an C<extension> value of C<RTIR>. 2410 2411=item site 2412 2413True if the file is considered a site-level override. For example, C<site> 2414will be false for C<RT_Config.pm> and true for C<RT_SiteConfig.pm>. 2415 2416=back 2417 2418=cut 2419 2420sub LoadedConfigs { 2421 # Copy to avoid the caller changing our internal data 2422 return map { \%$_ } @LOADED_CONFIGS 2423} 2424 2425=head2 Get 2426 2427Takes name of the option as argument and returns its current value. 2428 2429In the case of a user-overridable option, first checks the user's 2430preferences before looking for site-wide configuration. 2431 2432Returns values from RT_SiteConfig, RT_Config and then the %META hash 2433of configuration variables's "Default" for this config variable, 2434in that order. 2435 2436Returns different things in scalar and array contexts. For scalar 2437options it's not that important, however for arrays and hash it's. 2438In scalar context returns references to arrays and hashes. 2439 2440Use C<scalar> perl's op to force context, especially when you use 2441C<(..., Argument => RT->Config->Get('ArrayOpt'), ...)> 2442as perl's '=>' op doesn't change context of the right hand argument to 2443scalar. Instead use C<(..., Argument => scalar RT->Config->Get('ArrayOpt'), ...)>. 2444 2445It's also important for options that have no default value(no default 2446in F<etc/RT_Config.pm>). If you don't force scalar context then you'll 2447get empty list and all your named args will be messed up. For example 2448C<(arg1 => 1, arg2 => RT->Config->Get('OptionDoesNotExist'), arg3 => 3)> 2449will result in C<(arg1 => 1, arg2 => 'arg3', 3)> what is most probably 2450unexpected, or C<(arg1 => 1, arg2 => RT->Config->Get('ArrayOption'), arg3 => 3)> 2451will result in C<(arg1 => 1, arg2 => 'element of option', 'another_one' => ..., 'arg3', 3)>. 2452 2453=cut 2454 2455sub Get { 2456 my ( $self, $name, $user ) = @_; 2457 2458 my $res; 2459 if ( $user && $user->id && $META{$name}->{'Overridable'} ) { 2460 my $prefs = $user->Preferences($RT::System); 2461 $res = $prefs->{$name} if $prefs; 2462 } 2463 $res = $OPTIONS{$name} unless defined $res; 2464 $res = $META{$name}->{'Default'} unless defined $res; 2465 return $self->_ReturnValue( $res, $META{$name}->{'Type'} || 'SCALAR' ); 2466} 2467 2468=head2 GetObfuscated 2469 2470the same as Get, except it returns Obfuscated value via Obfuscate sub 2471 2472=cut 2473 2474sub GetObfuscated { 2475 my $self = shift; 2476 my ( $name, $user ) = @_; 2477 my $obfuscate = $META{$name}->{Obfuscate}; 2478 2479 # we use two Get here is to simplify the logic of the return value 2480 # configs need obfuscation are supposed to be less, so won't be too heavy 2481 2482 return $self->Get(@_) unless $obfuscate; 2483 2484 my $res = Clone::clone( $self->Get( @_ ) ); 2485 $res = $obfuscate->( $self, $res, $user && $user->Id ? $user : RT->SystemUser ); 2486 return $self->_ReturnValue( $res, $META{$name}->{'Type'} || 'SCALAR' ); 2487} 2488 2489=head2 Set 2490 2491Set option's value to new value. Takes name of the option and new value. 2492Returns old value. 2493 2494The new value should be scalar, array or hash depending on type of the option. 2495If the option is not defined in meta or the default RT config then it is of 2496scalar type. 2497 2498=cut 2499 2500sub Set { 2501 my ( $self, $name ) = ( shift, shift ); 2502 2503 my $old = $OPTIONS{$name}; 2504 my $type = $META{$name}->{'Type'} || 'SCALAR'; 2505 if ( $type eq 'ARRAY' ) { 2506 $OPTIONS{$name} = [@_]; 2507 { no warnings 'once'; no strict 'refs'; @{"RT::$name"} = (@_); } 2508 } elsif ( $type eq 'HASH' ) { 2509 $OPTIONS{$name} = {@_}; 2510 { no warnings 'once'; no strict 'refs'; %{"RT::$name"} = (@_); } 2511 } else { 2512 $OPTIONS{$name} = shift; 2513 {no warnings 'once'; no strict 'refs'; ${"RT::$name"} = $OPTIONS{$name}; } 2514 } 2515 $META{$name}->{'Type'} = $type; 2516 $META{$name}->{'PostSet'}->($self, $OPTIONS{$name}, $old) 2517 if $META{$name}->{'PostSet'}; 2518 if ($META{$name}->{'Deprecated'}) { 2519 my %deprecated = %{$META{$name}->{'Deprecated'}}; 2520 my $new_var = $deprecated{Instead} || ''; 2521 $self->SetFromConfig( 2522 Option => \$new_var, 2523 Value => [$OPTIONS{$name}], 2524 %{$self->Meta($name)->{'Source'}} 2525 ) if $new_var; 2526 $META{$name}->{'PostLoadCheck'} ||= sub { 2527 RT->Deprecated( 2528 Message => "Configuration option $name is deprecated", 2529 Stack => 0, 2530 %deprecated, 2531 ); 2532 }; 2533 } 2534 return $self->_ReturnValue( $old, $type ); 2535} 2536 2537sub _ReturnValue { 2538 my ( $self, $res, $type ) = @_; 2539 return $res unless wantarray; 2540 2541 if ( $type eq 'ARRAY' ) { 2542 return @{ $res || [] }; 2543 } elsif ( $type eq 'HASH' ) { 2544 return %{ $res || {} }; 2545 } 2546 return $res; 2547} 2548 2549sub SetFromConfig { 2550 my $self = shift; 2551 my %args = ( 2552 Option => undef, 2553 Value => [], 2554 Package => 'RT', 2555 File => '', 2556 Line => 0, 2557 SiteConfig => 1, 2558 Extension => 0, 2559 @_ 2560 ); 2561 2562 unless ( $args{'File'} ) { 2563 ( $args{'Package'}, $args{'File'}, $args{'Line'} ) = caller(1); 2564 } 2565 2566 my $opt = $args{'Option'}; 2567 2568 my $type; 2569 my $name = Symbol::Global::Name->find($opt); 2570 if ($name) { 2571 $type = ref $opt; 2572 $name =~ s/.*:://; 2573 } else { 2574 $name = $$opt; 2575 $type = $META{$name}->{'Type'} || 'SCALAR'; 2576 } 2577 2578 # if option is already set we have to check where 2579 # it comes from and may be ignore it 2580 if ( exists $OPTIONS{$name} ) { 2581 if ( $type eq 'HASH' ) { 2582 $args{'Value'} = [ 2583 @{ $args{'Value'} }, 2584 @{ $args{'Value'} }%2? (undef) : (), 2585 $self->Get( $name ), 2586 ]; 2587 } elsif ( $args{'SiteConfig'} && $args{'Extension'} ) { 2588 # if it's site config of an extension then it can only 2589 # override options that came from its main config 2590 if ( $args{'Extension'} ne $META{$name}->{'Source'}{'Extension'} ) { 2591 my %source = %{ $META{$name}->{'Source'} }; 2592 push @PreInitLoggerMessages, 2593 "Change of config option '$name' at $args{'File'} line $args{'Line'} has been ignored." 2594 ." This option earlier has been set in $source{'File'} line $source{'Line'}." 2595 ." To overide this option use ". ($source{'Extension'}||'RT') 2596 ." site config." 2597 ; 2598 return 1; 2599 } 2600 } elsif ( !$args{'SiteConfig'} && $META{$name}->{'Source'}{'SiteConfig'} ) { 2601 # if it's core config then we can override any option that came from another 2602 # core config, but not site config 2603 2604 my %source = %{ $META{$name}->{'Source'} }; 2605 if ( $source{'Extension'} ne $args{'Extension'} ) { 2606 # as a site config is loaded earlier then its base config 2607 # then we warn only on different extensions, for example 2608 # RTIR's options is set in main site config 2609 push @PreInitLoggerMessages, 2610 "Change of config option '$name' at $args{'File'} line $args{'Line'} has been ignored." 2611 ." It may be ok, but we want you to be aware." 2612 ." This option has been set earlier in $source{'File'} line $source{'Line'}." 2613 ; 2614 } 2615 2616 return 1; 2617 } 2618 } 2619 2620 $META{$name}->{'Type'} = $type; 2621 foreach (qw(Package File Line SiteConfig Extension Database)) { 2622 $META{$name}->{'Source'}->{$_} = $args{$_}; 2623 } 2624 $self->Set( $name, @{ $args{'Value'} } ); 2625 2626 return 1; 2627} 2628 2629=head2 Metadata 2630 2631 2632=head2 Meta 2633 2634=cut 2635 2636sub Meta { 2637 return $META{ $_[1] }; 2638} 2639 2640sub Sections { 2641 my $self = shift; 2642 my %seen; 2643 my @sections = sort 2644 grep !$seen{$_}++, 2645 map $_->{'Section'} || 'General', 2646 values %META; 2647 return @sections; 2648} 2649 2650sub Options { 2651 my $self = shift; 2652 my %args = ( Section => undef, Overridable => 1, Sorted => 1, @_ ); 2653 my @res = sort keys %META; 2654 2655 @res = grep( ( $META{$_}->{'Section'} || 'General' ) eq $args{'Section'}, 2656 @res 2657 ) if defined $args{'Section'}; 2658 2659 if ( defined $args{'Overridable'} ) { 2660 @res 2661 = grep( ( $META{$_}->{'Overridable'} || 0 ) == $args{'Overridable'}, 2662 @res ); 2663 } 2664 2665 if ( $args{'Sorted'} ) { 2666 @res = sort { 2667 ($META{$a}->{SortOrder}||9999) <=> ($META{$b}->{SortOrder}||9999) 2668 || $a cmp $b 2669 } @res; 2670 } else { 2671 @res = sort { $a cmp $b } @res; 2672 } 2673 return @res; 2674} 2675 2676=head2 AddOption( Name => '', Section => '', ... ) 2677 2678=cut 2679 2680sub AddOption { 2681 my $self = shift; 2682 my %args = ( 2683 Name => undef, 2684 Section => undef, 2685 Overridable => 0, 2686 SortOrder => undef, 2687 Widget => '/Widgets/Form/String', 2688 WidgetArguments => {}, 2689 @_ 2690 ); 2691 2692 unless ( $args{Name} ) { 2693 $RT::Logger->error("Need Name to add a new config"); 2694 return; 2695 } 2696 2697 unless ( $args{Section} ) { 2698 $RT::Logger->error("Need Section to add a new config option"); 2699 return; 2700 } 2701 2702 $META{ delete $args{Name} } = \%args; 2703} 2704 2705=head2 DeleteOption( Name => '' ) 2706 2707=cut 2708 2709sub DeleteOption { 2710 my $self = shift; 2711 my %args = ( 2712 Name => undef, 2713 @_ 2714 ); 2715 if ( $args{Name} ) { 2716 delete $META{$args{Name}}; 2717 } 2718 else { 2719 $RT::Logger->error("Need Name to remove a config option"); 2720 return; 2721 } 2722} 2723 2724=head2 UpdateOption( Name => '' ), Section => '', ... ) 2725 2726=cut 2727 2728sub UpdateOption { 2729 my $self = shift; 2730 my %args = ( 2731 Name => undef, 2732 Section => undef, 2733 Overridable => undef, 2734 SortOrder => undef, 2735 Widget => undef, 2736 WidgetArguments => undef, 2737 @_ 2738 ); 2739 2740 my $name = delete $args{Name}; 2741 2742 unless ( $name ) { 2743 $RT::Logger->error("Need Name to update a new config"); 2744 return; 2745 } 2746 2747 unless ( exists $META{$name} ) { 2748 $RT::Logger->error("Config $name doesn't exist"); 2749 return; 2750 } 2751 2752 for my $type ( keys %args ) { 2753 next unless defined $args{$type}; 2754 $META{$name}{$type} = $args{$type}; 2755 } 2756 return 1; 2757} 2758 2759sub ObjectHasCustomFieldGrouping { 2760 my $self = shift; 2761 my %args = ( Object => undef, Grouping => undef, @_ ); 2762 my $object_type = RT::CustomField->_GroupingClass($args{Object}); 2763 my $groupings = RT->Config->Get( 'CustomFieldGroupings' ); 2764 return 0 unless $groupings; 2765 return 1 if $groupings->{$object_type} && grep { $_ eq $args{Grouping} } @{ $groupings->{$object_type} }; 2766 return 0; 2767} 2768 2769# Internal method to activate ExtneralAuth if any ExternalAuth config 2770# options are set. 2771sub EnableExternalAuth { 2772 my $self = shift; 2773 2774 $self->Set('ExternalAuth', 1); 2775 require RT::Authen::ExternalAuth; 2776 return; 2777} 2778 2779my $database_config_cache_time = 0; 2780my %original_setting_from_files; 2781my $in_config_change_txn = 0; 2782 2783sub BeginDatabaseConfigChanges { 2784 $in_config_change_txn = $in_config_change_txn + 1; 2785} 2786 2787sub EndDatabaseConfigChanges { 2788 $in_config_change_txn = $in_config_change_txn - 1; 2789 if (!$in_config_change_txn) { 2790 shift->ApplyConfigChangeToAllServerProcesses(); 2791 } 2792} 2793 2794sub ApplyConfigChangeToAllServerProcesses { 2795 my $self = shift; 2796 2797 return if $in_config_change_txn; 2798 2799 # first apply locally 2800 $self->LoadConfigFromDatabase(); 2801 2802 # then notify other servers 2803 RT->System->ConfigCacheNeedsUpdate($database_config_cache_time); 2804} 2805 2806sub RefreshConfigFromDatabase { 2807 my $self = shift; 2808 if ($in_config_change_txn) { 2809 RT->Logger->error("It appears that there were unbalanced calls to BeginDatabaseConfigChanges with EndDatabaseConfigChanges; this indicates a software fault"); 2810 $in_config_change_txn = 0; 2811 } 2812 2813 if( RT->InstallMode ) { return; } # RT can't load the config in the DB if the DB is not there! 2814 my $needs_update = RT->System->ConfigCacheNeedsUpdate; 2815 if ($needs_update > $database_config_cache_time) { 2816 $self->LoadConfigFromDatabase(); 2817 $HTML::Mason::Commands::ReloadScrubber = 1; 2818 $database_config_cache_time = $needs_update; 2819 } 2820} 2821 2822sub LoadConfigFromDatabase { 2823 my $self = shift; 2824 2825 $database_config_cache_time = time; 2826 2827 my $settings = RT::Configurations->new(RT->SystemUser); 2828 $settings->LimitToEnabled; 2829 2830 my %seen; 2831 2832 while (my $setting = $settings->Next) { 2833 my $name = $setting->Name; 2834 my ($value, $error) = $setting->DecodedContent; 2835 next if $error; 2836 2837 if (!exists $original_setting_from_files{$name}) { 2838 $original_setting_from_files{$name} = [ 2839 scalar($self->Get($name)), 2840 Clone::clone(scalar($self->Meta($name))), 2841 ]; 2842 } 2843 2844 $seen{$name}++; 2845 2846 # are we inadvertantly overriding RT_SiteConfig.pm? 2847 my $meta = $META{$name}; 2848 if ($meta->{'Source'}) { 2849 my %source = %{ $meta->{'Source'} }; 2850 if ($source{'SiteConfig'} && $source{'File'} ne 'database') { 2851 push @PreInitLoggerMessages, 2852 "Change of config option '$name' at $source{File} line $source{Line} has been overridden by the config setting from the database. " 2853 ."Please remove it from $source{File} or from the database to avoid confusion."; 2854 } 2855 } 2856 2857 my $type = $meta->{Type} || 'SCALAR'; 2858 2859 my $val = $type eq 'ARRAY' ? $value 2860 : $type eq 'HASH' ? [ %$value ] 2861 : [ $value ]; 2862 2863 # hashes combine, but by default previous config settings shadow 2864 # later changes, here we want database configs to shadow file ones. 2865 if ($type eq 'HASH') { 2866 $val = [ $self->Get($name), @$val ]; 2867 $self->Set($name, ()); 2868 } 2869 2870 $self->SetFromConfig( 2871 Option => \$name, 2872 Value => $val, 2873 Package => 'N/A', 2874 File => 'database', 2875 Line => 'N/A', 2876 Database => 1, 2877 SiteConfig => 1, 2878 ); 2879 } 2880 2881 # anything that wasn't loaded from the database but has been set in 2882 # %original_setting_from_files must have been disabled from the database, 2883 # so we want to restore the original setting 2884 for my $name (keys %original_setting_from_files) { 2885 next if $seen{$name}; 2886 2887 my ($value, $meta) = @{ $original_setting_from_files{$name} }; 2888 my $type = $meta->{Type} || 'SCALAR'; 2889 2890 if ($type eq 'ARRAY') { 2891 $self->Set($name, @$value); 2892 } 2893 elsif ($type eq 'HASH') { 2894 $self->Set($name, %$value); 2895 } 2896 else { 2897 $self->Set($name, $value); 2898 } 2899 2900 %{ $META{$name} } = %$meta; 2901 } 2902} 2903 2904sub _GetFromFilesOnly { 2905 my ( $self, $name ) = @_; 2906 return $original_setting_from_files{$name} ? $original_setting_from_files{$name}[0] : undef; 2907} 2908 2909RT::Base->_ImportOverlays(); 2910 29111; 2912