X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=rt%2Flib%2FRT.pm;h=069309dc77a2ec858ad31f78139ff6d3eedffa1c;hp=50723765e498c7ab2a38c7844e372a4ffc2fe93c;hb=7322f2afedcc2f427e997d1535a503613a83f088;hpb=01f60974743197ac14e569c16c68a0c2ff3a5bd4 diff --git a/rt/lib/RT.pm b/rt/lib/RT.pm index 50723765e..069309dc7 100644 --- a/rt/lib/RT.pm +++ b/rt/lib/RT.pm @@ -2,7 +2,7 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2016 Best Practical Solutions, LLC # # # (Except where explicitly superseded by other copyright notices) @@ -48,90 +48,98 @@ use strict; use warnings; +use 5.010; package RT; +use Encode (); use File::Spec (); use Cwd (); +use Scalar::Util qw(blessed); +use UNIVERSAL::require; + +use vars qw($Config $System $SystemUser $Nobody $Handle $Logger $_Privileged $_Unprivileged $_INSTALL_MODE); + +use vars qw($BasePath + $EtcPath + $BinPath + $SbinPath + $VarPath + $FontPath + $LexiconPath + $StaticPath + $PluginPath + $LocalPath + $LocalEtcPath + $LocalLibPath + $LocalLexiconPath + $LocalStaticPath + $LocalPluginPath + $MasonComponentRoot + $MasonLocalComponentRoot + $MasonDataDir + $MasonSessionDir); + + +RT->LoadGeneratedData(); -use vars qw($Config $System $SystemUser $Nobody $Handle $Logger $_INSTALL_MODE); +=head1 NAME -our $VERSION = '3.8.10'; +RT - Request Tracker +=head1 SYNOPSIS +A fully featured request tracker package. -our $BasePath = '/opt/rt3'; -our $EtcPath = '/opt/rt3/etc'; -our $BinPath = '/opt/rt3/bin'; -our $SbinPath = '/opt/rt3/sbin'; -our $VarPath = '/opt/rt3/var'; -our $PluginPath = ''; -our $LocalPath = '/opt/rt3/local'; -our $LocalEtcPath = '/opt/rt3/local/etc'; -our $LocalLibPath = '/opt/rt3/local/lib'; -our $LocalLexiconPath = '/opt/rt3/local/po'; -our $LocalPluginPath = $LocalPath."/plugins"; +This documentation describes the point-of-entry for RT's Perl API. To learn +more about what RT is and what it can do for you, visit +L. +=head1 DESCRIPTION -# $MasonComponentRoot is where your rt instance keeps its mason html files +=head2 INITIALIZATION -our $MasonComponentRoot = '/var/www/freeside/rt'; +If you're using RT's Perl libraries, you need to initialize RT before using any +of the modules. -# $MasonLocalComponentRoot is where your rt instance keeps its site-local -# mason html files. +You have the option of handling the timing of config loading and the actual +init sequence yourself with: -our $MasonLocalComponentRoot = '/opt/rt3/local/html'; + use RT; + BEGIN { + RT->LoadConfig; + RT->Init; + } -# $MasonDataDir Where mason keeps its datafiles +or you can let RT do it all: -our $MasonDataDir = '/usr/local/etc/freeside/masondata'; + use RT -init; -# RT needs to put session data (for preserving state between connections -# via the web interface) -our $MasonSessionDir = '/opt/rt3/var/session_data'; +This second method is particular useful when writing one-liners to interact with RT: -unless ( File::Spec->file_name_is_absolute($EtcPath) ) { + perl -MRT=-init -e '...' -# if BasePath exists and is absolute, we won't infer it from $INC{'RT.pm'}. -# otherwise RT.pm will make src dir(where we configure RT) be the BasePath -# instead of the --prefix one - unless ( -d $BasePath && File::Spec->file_name_is_absolute($BasePath) ) { - my $pm_path = ( File::Spec->splitpath( $INC{'RT.pm'} ) )[1]; +The first method is necessary if you need to delay or conditionalize +initialization or if you want to fiddle with C<< RT->Config >> between loading +the config files and initializing the RT environment. - # need rel2abs here is to make sure path is absolute, since $INC{'RT.pm'} - # is not always absolute - $BasePath = - File::Spec->rel2abs( - File::Spec->catdir( $pm_path, File::Spec->updir ) ); - } +=cut - $BasePath = Cwd::realpath( $BasePath ); +{ + my $DID_IMPORT_INIT; + sub import { + my $class = shift; + my $action = shift || ''; - for my $path ( qw/EtcPath BinPath SbinPath VarPath LocalPath LocalEtcPath - LocalLibPath LocalLexiconPath PluginPath LocalPluginPath - MasonComponentRoot MasonLocalComponentRoot MasonDataDir - MasonSessionDir/ ) { - no strict 'refs'; - # just change relative ones - $$path = File::Spec->catfile( $BasePath, $$path ) - unless File::Spec->file_name_is_absolute( $$path ); + if ($action eq "-init" and not $DID_IMPORT_INIT) { + $class->LoadConfig; + $class->Init; + $DID_IMPORT_INIT = 1; + } } } - -=head1 NAME - -RT - Request Tracker - -=head1 SYNOPSIS - -A fully featured request tracker package - -=head1 DESCRIPTION - -=head2 INITIALIZATION - =head2 LoadConfig Load RT's config file. First, the site configuration file @@ -149,7 +157,7 @@ have not been set already. sub LoadConfig { require RT::Config; - $Config = new RT::Config; + $Config = RT::Config->new; $Config->LoadConfigs; require RT::I18N; @@ -157,30 +165,27 @@ sub LoadConfig { # If the user does that, do what they mean. $RT::WebPath = '' if ($RT::WebPath eq '/'); - # fix relative LogDir and GnuPG homedir + # Fix relative LogDir; It cannot be fixed in a PostLoadCheck, as + # they are run after logging is enabled. unless ( File::Spec->file_name_is_absolute( $Config->Get('LogDir') ) ) { $Config->Set( LogDir => File::Spec->catfile( $BasePath, $Config->Get('LogDir') ) ); } - my $gpgopts = $Config->Get('GnuPGOptions'); - unless ( File::Spec->file_name_is_absolute( $gpgopts->{homedir} ) ) { - $gpgopts->{homedir} = File::Spec->catfile( $BasePath, $gpgopts->{homedir} ); - } - - RT::I18N->Init; + return $Config; } =head2 Init -L, L, -L and L. +L, L, L, L, and L. =cut sub Init { - - my @arg = @_; + shift if @_%2; # code is inconsistent about calling as method + my %args = (@_); CheckPerlRequirements(); @@ -189,22 +194,24 @@ sub Init { #Get a database connection ConnectToDatabase(); InitSystemObjects(); - InitClasses(); - InitLogging(@arg); + InitClasses(%args); + InitLogging(%args); InitPlugins(); + _BuildTableAttributes(); + RT::I18N->Init; RT->Config->PostLoadCheck; - + RT::Lifecycle->FillCache; } =head2 ConnectToDatabase -Get a database connection. See also . +Get a database connection. See also L. =cut sub ConnectToDatabase { require RT::Handle; - $Handle = new RT::Handle unless $Handle; + $Handle = RT::Handle->new unless $Handle; $Handle->Connect; return $Handle; } @@ -233,7 +240,7 @@ sub InitLogging { warning => 3, error => 4, 'err' => 4, critical => 5, crit => 5, - alert => 6, + alert => 6, emergency => 7, emerg => 7, ); @@ -260,8 +267,11 @@ sub InitLogging { $frame++ while caller($frame) && caller($frame) =~ /^Log::/; my ($package, $filename, $line) = caller($frame); + # Encode to bytes, so we don't send wide characters + $p{message} = Encode::encode("UTF-8", $p{message}); + $p{'message'} =~ s/(?:\r*\n)+$//; - return "[". gmtime(time) ."] [". $p{'level'} ."]: " + return "[$$] [". gmtime(time) ."] [". $p{'level'} ."]: " . $p{'message'} ." ($filename:$line)\n"; }; @@ -275,14 +285,14 @@ sub InitLogging { $frame++ while caller($frame) && caller($frame) =~ /^Log::/; my ($package, $filename, $line) = caller($frame); - # syswrite() cannot take utf8; turn it off here. - Encode::_utf8_off($p{message}); + # Encode to bytes, so we don't send wide characters + $p{message} = Encode::encode("UTF-8", $p{message}); $p{message} =~ s/(?:\r*\n)+$//; if ($p{level} eq 'debug') { - return "$p{message}\n"; + return "[$$] $p{message} ($filename:$line)\n"; } else { - return "$p{message} ($filename:$line)\n"; + return "[$$] $p{message}\n"; } }; @@ -290,7 +300,7 @@ sub InitLogging { no warnings; my %p = @_; return $p{'message'} unless $level_to_num{ $p{'level'} } >= $stack_from_level; - + require Devel::StackTrace; my $trace = Devel::StackTrace->new( ignore_class => [ 'Log::Dispatch', 'Log::Dispatch::Base' ] ); return $p{'message'} . $trace->as_string; @@ -333,11 +343,11 @@ sub InitLogging { callbacks => [ $simple_cb, $stack_cb ], )); } - if ( $Config->Get('LogToScreen') ) { + if ( $Config->Get('LogToSTDERR') ) { require Log::Dispatch::Screen; $RT::Logger->add( Log::Dispatch::Screen->new ( name => 'screen', - min_level => $Config->Get('LogToScreen'), + min_level => $Config->Get('LogToSTDERR'), callbacks => [ $simple_cb, $stack_cb ], stderr => 1, )); @@ -360,6 +370,7 @@ sub InitLogging { sub InitSignalHandlers { my %arg = @_; + return if $arg{'NoSignalHandlers'}; # Signal handlers ## This is the default handling of warnings and die'ings in the code @@ -367,38 +378,30 @@ sub InitSignalHandlers { ## Mason). It will log all problems through the standard logging ## mechanism (see above). - unless ( $arg{'NoSignalHandlers'} ) { + $SIG{__WARN__} = sub { + # use 'goto &foo' syntax to hide ANON sub from stack + unshift @_, $RT::Logger, qw(level warning message); + goto &Log::Dispatch::log; + }; - $SIG{__WARN__} = sub { - # The 'wide character' warnings has to be silenced for now, at least - # until HTML::Mason offers a sane way to process both raw output and - # unicode strings. - # use 'goto &foo' syntax to hide ANON sub from stack - if( index($_[0], 'Wide character in ') != 0 ) { - unshift @_, $RT::Logger, qw(level warning message); - goto &Log::Dispatch::log; - } - }; - - #When we call die, trap it and log->crit with the value of the die. - - $SIG{__DIE__} = sub { - # if we are not in eval and perl is not parsing code - # then rollback transactions and log RT error - unless ($^S || !defined $^S ) { - $RT::Handle->Rollback(1) if $RT::Handle; - $RT::Logger->crit("$_[0]") if $RT::Logger; - } - die $_[0]; - }; +#When we call die, trap it and log->crit with the value of the die. - } + $SIG{__DIE__} = sub { + # if we are not in eval and perl is not parsing code + # then rollback transactions and log RT error + unless ($^S || !defined $^S ) { + $RT::Handle->Rollback(1) if $RT::Handle; + $RT::Logger->crit("$_[0]") if $RT::Logger; + } + die $_[0]; + }; } sub CheckPerlRequirements { - if ($^V < 5.008003) { - die sprintf "RT requires Perl v5.8.3 or newer. Your current Perl is v%vd\n", $^V; + eval {require 5.010_001}; + if ($@) { + die sprintf "RT requires Perl v5.10.1 or newer. Your current Perl is v%vd\n", $^V; } # use $error here so the following "die" can still affect the global $@ @@ -418,14 +421,14 @@ sub CheckPerlRequirements { die <<"EOF"; RT requires the Scalar::Util module be built with support for the 'weaken' -function. +function. -It is sometimes the case that operating system upgrades will replace +It is sometimes the case that operating system upgrades will replace a working Scalar::Util with a non-working one. If your system was working correctly up until now, this is likely the cause of the problem. -Please reinstall Scalar::Util, being careful to let it build with your C -compiler. Ususally this is as simple as running the following command as +Please reinstall Scalar::Util, being careful to let it build with your C +compiler. Usually this is as simple as running the following command as root. perl -MCPAN -e'install Scalar::Util' @@ -464,11 +467,52 @@ sub InitClasses { require RT::Attributes; require RT::Dashboard; require RT::Approval; + require RT::Lifecycle; + require RT::Link; + require RT::Links; + require RT::Article; + require RT::Articles; + require RT::Class; + require RT::Classes; + require RT::ObjectClass; + require RT::ObjectClasses; + require RT::ObjectTopic; + require RT::ObjectTopics; + require RT::Topic; + require RT::Topics; + require RT::Link; + require RT::Links; + + _BuildTableAttributes(); + + if ( $args{'Heavy'} ) { + # load scrips' modules + my $scrips = RT::Scrips->new(RT->SystemUser); + while ( my $scrip = $scrips->Next ) { + local $@; + eval { $scrip->LoadModules } or + $RT::Logger->error("Invalid Scrip ".$scrip->Id.". Unable to load the Action or Condition. ". + "You should delete or repair this Scrip in the admin UI.\n$@\n"); + } + + foreach my $class ( grep $_, RT->Config->Get('CustomFieldValuesSources') ) { + $class->require or $RT::Logger->error( + "Class '$class' is listed in CustomFieldValuesSources option" + ." in the config, but we failed to load it:\n$@\n" + ); + } + + } +} +sub _BuildTableAttributes { # on a cold server (just after restart) people could have an object # in the session, as we deserialize it so we never call constructor # of the class, so the list of accessible fields is empty and we die # with "Method xxx is not implemented in RT::SomeClass" + + # without this, we also can never call _ClassAccessible, because we + # won't have filled RT::Record::_TABLE_ATTR $_->_BuildTableAttributes foreach qw( RT::Ticket RT::Transaction @@ -480,6 +524,7 @@ sub InitClasses { RT::ScripAction RT::ScripCondition RT::Scrip + RT::ObjectScrip RT::Group RT::GroupMember RT::CustomField @@ -487,35 +532,20 @@ sub InitClasses { RT::ObjectCustomField RT::ObjectCustomFieldValue RT::Attribute + RT::ACE + RT::Article + RT::Class + RT::Link + RT::ObjectClass + RT::ObjectTopic + RT::Topic ); - - if ( $args{'Heavy'} ) { - # load scrips' modules - my $scrips = RT::Scrips->new($RT::SystemUser); - $scrips->Limit( FIELD => 'Stage', OPERATOR => '!=', VALUE => 'Disabled' ); - while ( my $scrip = $scrips->Next ) { - local $@; - eval { $scrip->LoadModules } or - $RT::Logger->error("Invalid Scrip ".$scrip->Id.". Unable to load the Action or Condition. ". - "You should delete or repair this Scrip in the admin UI.\n$@\n"); - } - - foreach my $class ( grep $_, RT->Config->Get('CustomFieldValuesSources') ) { - local $@; - eval "require $class; 1" or $RT::Logger->error( - "Class '$class' is listed in CustomFieldValuesSources option" - ." in the config, but we failed to load it:\n$@\n" - ); - } - - RT::I18N->LoadLexicons; - } } =head2 InitSystemObjects -Initializes system objects: C<$RT::System>, C<$RT::SystemUser> -and C<$RT::Nobody>. +Initializes system objects: C<$RT::System>, C<< RT->SystemUser >> +and C<< RT->Nobody >>. =cut @@ -523,11 +553,11 @@ sub InitSystemObjects { #RT's system user is a genuine database user. its id lives here require RT::CurrentUser; - $SystemUser = new RT::CurrentUser; + $SystemUser = RT::CurrentUser->new; $SystemUser->LoadByName('RT_System'); #RT's "nobody user" is a genuine database user. its ID lives here. - $Nobody = new RT::CurrentUser; + $Nobody = RT::CurrentUser->new; $Nobody->LoadByName('Nobody'); require RT::System; @@ -538,19 +568,19 @@ sub InitSystemObjects { =head2 Config -Returns the current L, but note that -you must L first otherwise this method +Returns the current L, but note that +you must L first otherwise this method returns undef. Method can be called as class method. =cut -sub Config { return $Config } +sub Config { return $Config || shift->LoadConfig(); } =head2 DatabaseHandle -Returns the current L. +Returns the current L. See also L. @@ -568,7 +598,7 @@ sub Logger { return $Logger } =head2 System -Returns the current L. See also +Returns the current L. See also L. =cut @@ -595,6 +625,23 @@ also L. sub Nobody { return $Nobody } +sub PrivilegedUsers { + if (!$_Privileged) { + $_Privileged = RT::Group->new(RT->SystemUser); + $_Privileged->LoadSystemInternalGroup('Privileged'); + } + return $_Privileged; +} + +sub UnprivilegedUsers { + if (!$_Unprivileged) { + $_Unprivileged = RT::Group->new(RT->SystemUser); + $_Unprivileged->LoadSystemInternalGroup('Unprivileged'); + } + return $_Unprivileged; +} + + =head2 Plugins Returns a listref of all Plugins currently configured for this RT instance. @@ -602,22 +649,27 @@ You can define plugins by adding them to the @Plugins list in your RT_SiteConfig =cut -our @PLUGINS = (); sub Plugins { + state @PLUGINS; + state $DID_INIT = 0; + my $self = shift; - unless (@PLUGINS) { + unless ($DID_INIT) { $self->InitPluginPaths; @PLUGINS = $self->InitPlugins; + $DID_INIT++; } - return \@PLUGINS; + return [@PLUGINS]; } =head2 PluginDirs -Takes optional subdir (e.g. po, lib, etc.) and return plugins' dirs that exist. +Takes an optional subdir (e.g. po, lib, etc.) and returns a list of +directories from plugins where that subdirectory exists. -This code chacke plugins names or anything else and required when main config -is loaded to load plugins' configs. +This code does not check plugin names, plugin validitity, or load +plugins (see L) in any way, and requires that RT's +configuration have been already loaded. =cut @@ -651,7 +703,9 @@ sub InitPluginPaths { my @tmp_inc; my $added; for (@INC) { - if ( Cwd::realpath($_) eq $RT::LocalLibPath) { + my $realpath = Cwd::realpath($_); + next unless defined $realpath; + if ( $realpath eq $RT::LocalLibPath) { push @tmp_inc, $_, @lib_dirs; $added = 1; } else { @@ -668,7 +722,8 @@ sub InitPluginPaths { =head2 InitPlugins -Initialze all Plugins found in the RT configuration file, setting up their lib and HTML::Mason component roots. +Initialize all Plugins found in the RT configuration file, setting up +their lib and L component roots. =cut @@ -688,15 +743,221 @@ sub InitPlugins { sub InstallMode { my $self = shift; if (@_) { - $_INSTALL_MODE = shift; - if($_INSTALL_MODE) { - require RT::CurrentUser; - $SystemUser = RT::CurrentUser->new(); - } + my ($integrity, $state, $msg) = RT::Handle->CheckIntegrity; + if ($_[0] and $integrity) { + # Trying to turn install mode on but we have a good DB! + require Carp; + $RT::Logger->error( + Carp::longmess("Something tried to turn on InstallMode but we have DB integrity!") + ); + } + else { + $_INSTALL_MODE = shift; + if($_INSTALL_MODE) { + require RT::CurrentUser; + $SystemUser = RT::CurrentUser->new(); + } + } } return $_INSTALL_MODE; } +sub LoadGeneratedData { + my $class = shift; + my $pm_path = ( File::Spec->splitpath( $INC{'RT.pm'} ) )[1]; + + require "$pm_path/RT/Generated.pm" || die "Couldn't load RT::Generated: $@"; + $class->CanonicalizeGeneratedPaths(); +} + +sub CanonicalizeGeneratedPaths { + my $class = shift; + unless ( File::Spec->file_name_is_absolute($EtcPath) ) { + + # if BasePath exists and is absolute, we won't infer it from $INC{'RT.pm'}. + # otherwise RT.pm will make the source dir(where we configure RT) be the + # BasePath instead of the one specified by --prefix + unless ( -d $BasePath + && File::Spec->file_name_is_absolute($BasePath) ) + { + my $pm_path = ( File::Spec->splitpath( $INC{'RT.pm'} ) )[1]; + + # need rel2abs here is to make sure path is absolute, since $INC{'RT.pm'} + # is not always absolute + $BasePath = File::Spec->rel2abs( + File::Spec->catdir( $pm_path, File::Spec->updir ) ); + } + + $BasePath = Cwd::realpath($BasePath); + + for my $path ( + qw/EtcPath BinPath SbinPath VarPath LocalPath StaticPath LocalEtcPath + LocalLibPath LexiconPath LocalLexiconPath PluginPath FontPath + LocalPluginPath LocalStaticPath MasonComponentRoot MasonLocalComponentRoot + MasonDataDir MasonSessionDir/ + ) + { + no strict 'refs'; + + # just change relative ones + $$path = File::Spec->catfile( $BasePath, $$path ) + unless File::Spec->file_name_is_absolute($$path); + } + } + +} + +=head2 AddJavaScript + +Helper method to add JS files to the C<@JSFiles> config at runtime. + +To add files, you can add the following line to your extension's main C<.pm> +file: + + RT->AddJavaScript( 'foo.js', 'bar.js' ); + +Files are expected to be in a static root in a F directory, such as +F in your extension or F for local overlays. + +=cut + +sub AddJavaScript { + my $self = shift; + + my @old = RT->Config->Get('JSFiles'); + RT->Config->Set( 'JSFiles', @old, @_ ); + return RT->Config->Get('JSFiles'); +} + +=head2 AddStyleSheets + +Helper method to add CSS files to the C<@CSSFiles> config at runtime. + +To add files, you can add the following line to your extension's main C<.pm> +file: + + RT->AddStyleSheets( 'foo.css', 'bar.css' ); + +Files are expected to be in a static root in a F directory, such as +F in your extension or F for local +overlays. + +=cut + +sub AddStyleSheets { + my $self = shift; + my @old = RT->Config->Get('CSSFiles'); + RT->Config->Set( 'CSSFiles', @old, @_ ); + return RT->Config->Get('CSSFiles'); +} + +=head2 JavaScript + +helper method of RT->Config->Get('JSFiles') + +=cut + +sub JavaScript { + return RT->Config->Get('JSFiles'); +} + +=head2 StyleSheets + +helper method of RT->Config->Get('CSSFiles') + +=cut + +sub StyleSheets { + return RT->Config->Get('CSSFiles'); +} + +=head2 Deprecated + +Notes that a particular call path is deprecated, and will be removed in +a particular release. Puts a warning in the logs indicating such, along +with a stack trace. + +Optional arguments include: + +=over + +=item Remove + +The release which is slated to remove the method or component + +=item Instead + +A suggestion of what to use in place of the deprecated API + +=item Arguments + +Used if not the entire method is being removed, merely a manner of +calling it; names the arguments which are deprecated. + +=item Message + +Overrides the auto-built phrasing of C with a custom message. + +=item Object + +An L object to print the class and numeric id of. Useful if the +admin will need to hunt down a particular object to fix the deprecation +warning. + +=back + +=cut + +sub Deprecated { + my $class = shift; + my %args = ( + Arguments => undef, + Remove => undef, + Instead => undef, + Message => undef, + Stack => 1, + LogLevel => "warn", + @_, + ); + + my ($function) = (caller(1))[3]; + my $stack; + if ($function eq "HTML::Mason::Commands::__ANON__") { + eval { HTML::Mason::Exception->throw() }; + my $error = $@; + my $info = $error->analyze_error; + $function = "Mason component ".$info->{frames}[0]->filename; + $stack = join("\n", map { sprintf("\t[%s:%d]", $_->filename, $_->line) } @{$info->{frames}}); + } else { + $function = "function $function"; + $stack = Carp::longmess(); + } + $stack =~ s/^.*?\n//; # Strip off call to ->Deprecated + + my $msg; + if ($args{Message}) { + $msg = $args{Message}; + } elsif ($args{Arguments}) { + $msg = "Calling $function with $args{Arguments} is deprecated"; + } else { + $msg = "The $function is deprecated"; + } + $msg .= ", and will be removed in RT $args{Remove}" + if $args{Remove}; + $msg .= "."; + + $msg .= " You should use $args{Instead} instead." + if $args{Instead}; + + $msg .= sprintf " Object: %s #%d.", blessed($args{Object}), $args{Object}->id + if $args{Object}; + + $msg .= " Call stack:\n$stack" if $args{Stack}; + + my $loglevel = $args{LogLevel}; + RT->Logger->$loglevel($msg); +} =head1 BUGS @@ -710,7 +971,6 @@ If you're not sure what's going on, report them rt-devel@lists.bestpractical.com L L - =cut require RT::Base;