rt 4.2.13 ticket#13852
[freeside.git] / rt / lib / RT.pm
index 4a20f9b..069309d 100644 (file)
@@ -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
 #                                          <sales@bestpractical.com>
 #
 # (Except where explicitly superseded by other copyright notices)
 
 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.11';
+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<https://bestpractical.com/rt>.
 
+=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<Connect to the database /ConnectToDatabase>, L<initilizes system objects /InitSystemObjects>,
-L<preloads classes /InitClasses> and L<set up logging /InitLogging>.
+L<Connects to the database|/ConnectToDatabase>, L<initilizes system
+objects|/InitSystemObjects>, L<preloads classes|/InitClasses>, L<sets
+up logging|/InitLogging>, and L<loads plugins|/InitPlugins>.
 
 =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 </Handle>.
+Get a database connection. See also L</Handle>.
 
 =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<config object RT::Config>, but note that
-you must L<load config /LoadConfig> first otherwise this method
+Returns the current L<config object|RT::Config>, but note that
+you must L<load config|/LoadConfig> 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<database handle object RT::Handle>.
+Returns the current L<database handle object|RT::Handle>.
 
 See also L</ConnectToDatabase>.
 
@@ -568,7 +598,7 @@ sub Logger { return $Logger }
 
 =head2 System
 
-Returns the current L<system object RT::System>. See also
+Returns the current L<system object|RT::System>. See also
 L</InitSystemObjects>.
 
 =cut
@@ -595,6 +625,23 @@ also L</InitSystemObjects>.
 
 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</InitPlugins>) 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<HTML::Mason> 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<js/> directory, such as
+F<static/js/> in your extension or F<local/static/js/> 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<css/> directory, such as
+F<static/css/> in your extension or F<local/static/css/> 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<Calling function ____ is
+deprecated> with a custom message.
+
+=item Object
+
+An L<RT::Record> 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<RT::StyleGuide>
 L<DBIx::SearchBuilder>
 
-
 =cut
 
 require RT::Base;