diff options
Diffstat (limited to 'rt/lib/RT/Interface')
-rw-r--r-- | rt/lib/RT/Interface/CLI.pm | 48 | ||||
-rwxr-xr-x | rt/lib/RT/Interface/Email.pm | 134 | ||||
-rwxr-xr-x | rt/lib/RT/Interface/Email/Auth/GnuPG.pm | 12 | ||||
-rw-r--r-- | rt/lib/RT/Interface/Email/Auth/MailFrom.pm | 13 | ||||
-rw-r--r-- | rt/lib/RT/Interface/Email/Filter/SpamAssassin.pm | 97 | ||||
-rw-r--r-- | rt/lib/RT/Interface/REST.pm | 21 | ||||
-rw-r--r-- | rt/lib/RT/Interface/Web.pm | 850 | ||||
-rw-r--r-- | rt/lib/RT/Interface/Web/Handler.pm | 218 | ||||
-rw-r--r-- | rt/lib/RT/Interface/Web/Menu.pm | 259 | ||||
-rw-r--r-- | rt/lib/RT/Interface/Web/Menu/Item.pm | 88 | ||||
-rwxr-xr-x | rt/lib/RT/Interface/Web/QueryBuilder.pm | 2 | ||||
-rwxr-xr-x | rt/lib/RT/Interface/Web/QueryBuilder/Tree.pm | 12 | ||||
-rw-r--r-- | rt/lib/RT/Interface/Web/Request.pm | 51 | ||||
-rw-r--r-- | rt/lib/RT/Interface/Web/Session.pm | 2 | ||||
-rwxr-xr-x | rt/lib/RT/Interface/Web/Standalone.pm | 126 | ||||
-rw-r--r-- | rt/lib/RT/Interface/Web/Standalone/PreFork.pm | 103 |
16 files changed, 1231 insertions, 805 deletions
diff --git a/rt/lib/RT/Interface/CLI.pm b/rt/lib/RT/Interface/CLI.pm index fe109d02a..bcdc13cdf 100644 --- a/rt/lib/RT/Interface/CLI.pm +++ b/rt/lib/RT/Interface/CLI.pm @@ -2,7 +2,7 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC # <sales@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -46,25 +46,12 @@ # # END BPS TAGGED BLOCK }}} +package RT::Interface::CLI; use strict; - use RT; -package RT::Interface::CLI; - - -BEGIN { - use base 'Exporter'; - use vars qw ($VERSION @EXPORT @EXPORT_OK %EXPORT_TAGS); - - # set the version for version checking - $VERSION = do { my @r = (q$Revision: 1.3 $ =~ /\d+/g); sprintf "%d."."%02d" x $#r, @r }; # must be all one line, for MakeMaker - - # your exported package globals go here, - # as well as any optionally exported functions - @EXPORT_OK = qw(&CleanEnv - &GetCurrentUser &GetMessageContent &debug &loc); -} +use base 'Exporter'; +our @EXPORT_OK = qw(CleanEnv GetCurrentUser GetMessageContent debug loc); =head1 NAME @@ -124,7 +111,6 @@ sub CleanEnv { my $CurrentUser; # shared betwen GetCurrentUser and loc -# {{{ sub GetCurrentUser =head2 GetCurrentUser @@ -144,7 +130,7 @@ sub GetCurrentUser { #If the current user is 0, then RT will assume that the User object #is that of the currentuser. - $CurrentUser = new RT::CurrentUser(); + $CurrentUser = RT::CurrentUser->new(); $CurrentUser->LoadByGecos($Gecos); unless ($CurrentUser->Id) { @@ -153,10 +139,8 @@ sub GetCurrentUser { return($CurrentUser); } -# }}} -# {{{ sub loc =head2 loc @@ -168,12 +152,10 @@ sub loc { die "No current user yet" unless $CurrentUser ||= RT::CurrentUser->new; return $CurrentUser->loc(@_); } -# }}} } -# {{{ sub GetMessageContent =head2 GetMessageContent @@ -200,7 +182,7 @@ sub GetMessageContent { #Load the sourcefile, if it's been handed to us if ($source) { - open( SOURCE, '<', $source ) or die $!; + open( SOURCE, '<', $source ) or die $!; @lines = (<SOURCE>) or die $!; close (SOURCE) or die $!; } @@ -235,9 +217,7 @@ sub GetMessageContent { } -# }}} -# {{{ sub debug sub debug { my $val = shift; @@ -253,8 +233,20 @@ sub debug { } } -# }}} - +sub ShowHelp { + my $self = shift; + my %args = @_; + require Pod::Usage; + Pod::Usage::pod2usage( + -message => $args{'Message'}, + -exitval => $args{'ExitValue'} || 0, + -verbose => 99, + -sections => $args{'Sections'} || ($args{'ExitValue'} + ? 'NAME|USAGE' + : 'NAME|USAGE|OPTIONS|DESCRIPTION' + ), + ); +} RT::Base->_ImportOverlays(); diff --git a/rt/lib/RT/Interface/Email.pm b/rt/lib/RT/Interface/Email.pm index 9216887cd..b9145d63a 100755 --- a/rt/lib/RT/Interface/Email.pm +++ b/rt/lib/RT/Interface/Email.pm @@ -2,7 +2,7 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC # <sales@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -387,7 +387,7 @@ sub SendEmail { unless ( $args{'Entity'}->head->get('Date') ) { require RT::Date; - my $date = RT::Date->new( $RT::SystemUser ); + my $date = RT::Date->new( RT->SystemUser ); $date->SetToNow; $args{'Entity'}->head->set( 'Date', $date->RFC2822( Timezone => 'server' ) ); } @@ -406,8 +406,10 @@ sub SendEmail { my $path = RT->Config->Get('SendmailPath'); my $args = RT->Config->Get('SendmailArguments'); - # SetOutgoingMailFrom - if ( RT->Config->Get('SetOutgoingMailFrom') ) { + # SetOutgoingMailFrom and bounces conflict, since they both want -f + if ( $args{'Bounce'} ) { + $args .= ' '. RT->Config->Get('SendmailBounceArguments'); + } elsif ( RT->Config->Get('SetOutgoingMailFrom') ) { my $OutgoingMailAddress; if ($TicketObj) { @@ -427,9 +429,6 @@ sub SendEmail { if $OutgoingMailAddress; } - # Set Bounce Arguments - $args .= ' '. RT->Config->Get('SendmailBounceArguments') if $args{'Bounce'}; - # VERP if ( $TransactionObj and my $prefix = RT->Config->Get('VERPPrefix') and @@ -555,7 +554,7 @@ sub PrepareEmailUsingTemplate { @_ ); - my $template = RT::Template->new( $RT::SystemUser ); + my $template = RT::Template->new( RT->SystemUser ); $template->LoadGlobalTemplate( $args{'Template'} ); unless ( $template->id ) { return (undef, "Couldn't load template '". $args{'Template'} ."'"); @@ -583,6 +582,7 @@ sub SendEmailUsingTemplate { Bcc => undef, From => RT->Config->Get('CorrespondAddress'), InReplyTo => undef, + ExtraHeaders => {}, @_ ); @@ -598,6 +598,9 @@ sub SendEmailUsingTemplate { $mail->head->set( $_ => Encode::encode_utf8( $args{ $_ } ) ) foreach grep defined $args{$_}, qw(To Cc Bcc From); + $mail->head->set( $_ => $args{ExtraHeaders}{$_} ) + foreach keys %{ $args{ExtraHeaders} }; + SetInReplyTo( Message => $mail, InReplyTo => $args{'InReplyTo'} ); return SendEmail( Entity => $mail ); @@ -615,7 +618,19 @@ sub ForwardTransaction { my $entity = $txn->ContentAsMIME; - return SendForward( %args, Entity => $entity, Transaction => $txn ); + my ( $ret, $msg ) = SendForward( %args, Entity => $entity, Transaction => $txn ); + if ($ret) { + my $ticket = $txn->TicketObj; + my ( $ret, $msg ) = $ticket->_NewTransaction( + Type => 'Forward Transaction', + Field => $txn->id, + Data => join ', ', grep { length } $args{To}, $args{Cc}, $args{Bcc}, + ); + unless ($ret) { + $RT::Logger->error("Failed to create transaction: $msg"); + } + } + return ( $ret, $msg ); } =head2 ForwardTicket TICKET, To => '', Cc => '', Bcc => '' @@ -635,13 +650,33 @@ sub ForwardTicket { ) for qw(Create Correspond); my $entity = MIME::Entity->build( - Type => 'multipart/mixed', + Type => 'multipart/mixed', + Description => 'forwarded ticket', ); $entity->add_part( $_ ) foreach map $_->ContentAsMIME, @{ $txns->ItemsArrayRef }; - return SendForward( %args, Entity => $entity, Ticket => $ticket, Template => 'Forward Ticket' ); + my ( $ret, $msg ) = SendForward( + %args, + Entity => $entity, + Ticket => $ticket, + Template => 'Forward Ticket', + ); + + if ($ret) { + my ( $ret, $msg ) = $ticket->_NewTransaction( + Type => 'Forward Ticket', + Field => $ticket->id, + Data => join ', ', grep { length } $args{To}, $args{Cc}, $args{Bcc}, + ); + unless ($ret) { + $RT::Logger->error("Failed to create transaction: $msg"); + } + } + + return ( $ret, $msg ); + } =head2 SendForward Entity => undef, Ticket => undef, Transaction => undef, Template => undef, To => '', Cc => '', Bcc => '' @@ -704,34 +739,52 @@ sub SendForward { $mail->head->set( $_ => EncodeToMIME( String => $args{$_} ) ) foreach grep defined $args{$_}, qw(To Cc Bcc); - $mail->attach( - Type => 'message/rfc822', - Disposition => 'attachment', - Description => 'forwarded message', - Data => $entity->as_string, - ); + $mail->make_multipart unless $mail->is_multipart; + $mail->add_part( $entity ); my $from; my $subject = ''; $subject = $txn->Subject if $txn; $subject ||= $ticket->Subject if $ticket; - if ( RT->Config->Get('ForwardFromUser') ) { - $from = ($txn || $ticket)->CurrentUser->UserObj->EmailAddress; - } else { + + unless ( RT->Config->Get('ForwardFromUser') ) { # XXX: what if want to forward txn of other object than ticket? $subject = AddSubjectTag( $subject, $ticket ); - $from = $ticket->QueueObj->CorrespondAddress - || RT->Config->Get('CorrespondAddress'); } + $mail->head->set( Subject => EncodeToMIME( String => "Fwd: $subject" ) ); - $mail->head->set( From => EncodeToMIME( String => $from ) ); + $mail->head->set( + From => EncodeToMIME( + String => GetForwardFrom( Transaction => $txn, Ticket => $ticket ) + ) + ); my $status = RT->Config->Get('ForwardFromUser') # never sign if we forward from User ? SendEmail( %args, Entity => $mail, Sign => 0 ) : SendEmail( %args, Entity => $mail ); return (0, $ticket->loc("Couldn't send email")) unless $status; - return (1, $ticket->loc("Send email successfully")); + return (1, $ticket->loc("Sent email successfully")); +} + +=head2 GetForwardFrom Ticket => undef, Transaction => undef + +Resolve the From field to use in forward mail + +=cut + +sub GetForwardFrom { + my %args = ( Ticket => undef, Transaction => undef, @_ ); + my $txn = $args{Transaction}; + my $ticket = $args{Ticket} || $txn->Object; + + if ( RT->Config->Get('ForwardFromUser') ) { + return ( $txn || $ticket )->CurrentUser->UserObj->EmailAddress; + } + else { + return $ticket->QueueObj->CorrespondAddress + || RT->Config->Get('CorrespondAddress'); + } } =head2 SignEncrypt Entity => undef, Sign => 0, Encrypt => 0 @@ -891,7 +944,7 @@ sub EncodeToMIME { return ($value); } - return ($value) unless $value =~ /[^\x20-\x7e]/; + return ($value) if $value =~ /^(?:[\t\x20-\x7e]|\x0D*\x0A[ \t])+$/s; $value =~ s/\s+$//; @@ -920,7 +973,7 @@ sub EncodeToMIME { sub CreateUser { my ( $Username, $Address, $Name, $ErrorsTo, $entity ) = @_; - my $NewUser = RT::User->new( $RT::SystemUser ); + my $NewUser = RT::User->new( RT->SystemUser ); my ( $Val, $Message ) = $NewUser->Create( Name => ( $Username || $Address ), @@ -955,7 +1008,7 @@ sub CreateUser { } #Load the new user object - my $CurrentUser = new RT::CurrentUser; + my $CurrentUser = RT::CurrentUser->new; $CurrentUser->LoadByEmail( $Address ); unless ( $CurrentUser->id ) { @@ -1087,12 +1140,7 @@ sub ParseAddressFromHeader { return ( undef, undef ); } - my $Name = ( $AddrObj->name || $AddrObj->phrase || $AddrObj->comment || $AddrObj->address ); - - #Lets take the from and load a user object. - my $Address = $AddrObj->address; - - return ( $Address, $Name ); + return ( $AddrObj->address, $AddrObj->phrase ); } =head2 DeleteRecipientsFromHead HEAD RECIPIENTS @@ -1195,7 +1243,7 @@ sub AddSubjectTag { my $subject = shift; my $ticket = shift; unless ( ref $ticket ) { - my $tmp = RT::Ticket->new( $RT::SystemUser ); + my $tmp = RT::Ticket->new( RT->SystemUser ); $tmp->Load( $ticket ); $ticket = $tmp; } @@ -1211,7 +1259,7 @@ sub AddSubjectTag { } return $subject if $subject =~ /\[$tag_re\s+#$id\]/; - $subject =~ s/(\r\n|\n|\s)/ /gi; + $subject =~ s/(\r\n|\n|\s)/ /g; chomp $subject; return "[". ($queue_tag || RT->Config->Get('rtname')) ." #$id] $subject"; } @@ -1263,7 +1311,7 @@ sub _LoadPlugins { } elsif ( !ref $plugin ) { my $Class = $plugin; $Class = "RT::Interface::Email::" . $Class - unless $Class =~ /^RT::Interface::Email::/; + unless $Class =~ /^RT::/; $Class->require or do { $RT::Logger->error("Couldn't load $Class: $@"); next }; @@ -1375,7 +1423,7 @@ sub Gateway { my $Subject = $head->get('Subject') || ''; chomp $Subject; - # {{{ Lets check for mail loops of various sorts. + # Lets check for mail loops of various sorts. my ($should_store_machine_generated_message, $IsALoop, $result); ( $should_store_machine_generated_message, $ErrorsTo, $result, $IsALoop ) = _HandleMachineGeneratedMail( @@ -1394,7 +1442,7 @@ sub Gateway { $args{'ticket'} ||= ParseTicketId( $Subject ); - $SystemTicket = RT::Ticket->new( $RT::SystemUser ); + $SystemTicket = RT::Ticket->new( RT->SystemUser ); $SystemTicket->Load( $args{'ticket'} ) if ( $args{'ticket'} ) ; if ( $SystemTicket->id ) { $Right = 'ReplyToTicket'; @@ -1403,7 +1451,7 @@ sub Gateway { } #Set up a queue object - my $SystemQueueObj = RT::Queue->new( $RT::SystemUser ); + my $SystemQueueObj = RT::Queue->new( RT->SystemUser ); $SystemQueueObj->Load( $args{'queue'} ); # We can safely have no queue of we have a known-good ticket @@ -1420,7 +1468,7 @@ sub Gateway { SystemQueue => $SystemQueueObj, ); - # {{{ If authentication fails and no new user was created, get out. + # If authentication fails and no new user was created, get out. if ( !$CurrentUser || !$CurrentUser->id || $AuthStat == -1 ) { # If the plugins refused to create one, they lose. @@ -1772,7 +1820,7 @@ sub _HandleMachineGeneratedMail { # Squelch replies if necessary # Don't let the user stuff the RT-Squelch-Replies-To header. if ( $head->get('RT-Squelch-Replies-To') ) { - $head->add( + $head->replace( 'RT-Relocated-Squelch-Replies-To', $head->get('RT-Squelch-Replies-To') ); @@ -1787,8 +1835,8 @@ sub _HandleMachineGeneratedMail { # to the scrip. We might want to notify nobody. Or just # the RT Owner. Or maybe all Privileged watchers. my ( $Sender, $junk ) = ParseSenderAddressFromHead($head); - $head->add( 'RT-Squelch-Replies-To', $Sender ); - $head->add( 'RT-DetectedAutoGenerated', 'true' ); + $head->replace( 'RT-Squelch-Replies-To', $Sender ); + $head->replace( 'RT-DetectedAutoGenerated', 'true' ); } return ( 1, $ErrorsTo, "Handled machine detection", $IsALoop ); } diff --git a/rt/lib/RT/Interface/Email/Auth/GnuPG.pm b/rt/lib/RT/Interface/Email/Auth/GnuPG.pm index 6d43b9610..e508908fb 100755 --- a/rt/lib/RT/Interface/Email/Auth/GnuPG.pm +++ b/rt/lib/RT/Interface/Email/Auth/GnuPG.pm @@ -2,7 +2,7 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC # <sales@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -58,7 +58,7 @@ To use the gnupg-secured mail gateway, you need to do the following: Set up a GnuPG key directory with a pubring containing only the keys you care about and specify the following in your SiteConfig.pm - Set(%GnuPGOptions, homedir => '/opt/rt3/var/data/GnuPG'); + Set(%GnuPGOptions, homedir => '/opt/rt4/var/data/GnuPG'); Set(@MailPlugins, 'Auth::MailFrom', 'Auth::GnuPG', ...other filter...); =cut @@ -88,7 +88,7 @@ sub GetCurrentUser { Entity => $args{'Message'}, AddStatus => 1, ); if ( $status && !@res ) { - $args{'Message'}->head->add( + $args{'Message'}->head->replace( 'X-RT-Incoming-Encryption' => 'Not encrypted' ); @@ -112,7 +112,7 @@ sub GetCurrentUser { Data => ${ $args{'RawMessageRef'} }, ); - $args{'Message'}->head->add( 'X-RT-Privacy' => 'PGP' ); + $args{'Message'}->head->replace( 'X-RT-Privacy' => 'PGP' ); foreach my $part ( $args{'Message'}->parts_DFS ) { my $decrypted; @@ -124,14 +124,14 @@ sub GetCurrentUser { $decrypted = 1; } if ( $_->{Operation} eq 'Verify' && $_->{Status} eq 'DONE' ) { - $part->head->add( + $part->head->replace( 'X-RT-Incoming-Signature' => $_->{UserString} ); } } } - $part->head->add( + $part->head->replace( 'X-RT-Incoming-Encryption' => $decrypted ? 'Success' : 'Not encrypted' ); diff --git a/rt/lib/RT/Interface/Email/Auth/MailFrom.pm b/rt/lib/RT/Interface/Email/Auth/MailFrom.pm index be2f517e1..e733bdaae 100644 --- a/rt/lib/RT/Interface/Email/Auth/MailFrom.pm +++ b/rt/lib/RT/Interface/Email/Auth/MailFrom.pm @@ -2,7 +2,7 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC # <sales@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -47,10 +47,12 @@ # END BPS TAGGED BLOCK }}} package RT::Interface::Email::Auth::MailFrom; -use RT::Interface::Email qw(ParseSenderAddressFromHead CreateUser); + use strict; use warnings; +use RT::Interface::Email qw(ParseSenderAddressFromHead CreateUser); + # This is what the ordinary, non-enhanced gateway does at the moment. sub GetCurrentUser { @@ -70,7 +72,7 @@ sub GetCurrentUser { return ( $args{'CurrentUser'}, -1 ); } - my $CurrentUser = new RT::CurrentUser; + my $CurrentUser = RT::CurrentUser->new; $CurrentUser->LoadByEmail( $Address ); $CurrentUser->LoadByName( $Address ) unless $CurrentUser->Id; if ( $CurrentUser->Id ) { @@ -79,14 +81,13 @@ sub GetCurrentUser { } # If the user can't be loaded, we may need to create one. Figure out the acl situation. - my $unpriv = RT::Group->new( $RT::SystemUser ); - $unpriv->LoadSystemInternalGroup('Unprivileged'); + my $unpriv = RT->UnprivilegedUsers(); unless ( $unpriv->Id ) { $RT::Logger->crit("Couldn't find the 'Unprivileged' internal group"); return ( $args{'CurrentUser'}, -1 ); } - my $everyone = RT::Group->new( $RT::SystemUser ); + my $everyone = RT::Group->new( RT->SystemUser ); $everyone->LoadSystemInternalGroup('Everyone'); unless ( $everyone->Id ) { $RT::Logger->crit("Couldn't find the 'Everyone' internal group"); diff --git a/rt/lib/RT/Interface/Email/Filter/SpamAssassin.pm b/rt/lib/RT/Interface/Email/Filter/SpamAssassin.pm deleted file mode 100644 index 0470e6340..000000000 --- a/rt/lib/RT/Interface/Email/Filter/SpamAssassin.pm +++ /dev/null @@ -1,97 +0,0 @@ -# BEGIN BPS TAGGED BLOCK {{{ -# -# COPYRIGHT: -# -# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC -# <sales@bestpractical.com> -# -# (Except where explicitly superseded by other copyright notices) -# -# -# LICENSE: -# -# This work is made available to you under the terms of Version 2 of -# the GNU General Public License. A copy of that license should have -# been provided with this software, but in any event can be snarfed -# from www.gnu.org. -# -# This work is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -# 02110-1301 or visit their web page on the internet at -# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. -# -# -# CONTRIBUTION SUBMISSION POLICY: -# -# (The following paragraph is not intended to limit the rights granted -# to you to modify and distribute this software under the terms of -# the GNU General Public License and is only of importance to you if -# you choose to contribute your changes and enhancements to the -# community by submitting them to Best Practical Solutions, LLC.) -# -# By intentionally submitting any modifications, corrections or -# derivatives to this work, or any other work intended for use with -# Request Tracker, to Best Practical Solutions, LLC, you confirm that -# you are the copyright holder for those contributions and you grant -# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, -# royalty-free, perpetual, license to use, copy, create derivative -# works based on those contributions, and sublicense and distribute -# those contributions and any derivatives thereof. -# -# END BPS TAGGED BLOCK }}} - -package RT::Interface::Email::Filter::SpamAssassin; -use strict; -use warnings; - -use Mail::SpamAssassin; -my $spamtest = Mail::SpamAssassin->new(); - -sub GetCurrentUser { - my %args = ( - Message => undef, - CurrentUser => undef, - AuthLevel => undef, - @_ - ); - my $status = $spamtest->check( $args{'Message'} ); - return ( $args{'CurrentUser'}, $args{'AuthLevel'} ) - unless $status->is_spam(); - - eval { $status->rewrite_mail() }; - if ( $status->get_hits > $status->get_required_hits() * 1.5 ) { - - # Spammy indeed - return ( $args{'CurrentUser'}, -1 ); - } - return ( $args{'CurrentUser'}, $args{'AuthLevel'} ); - -} - -=head1 NAME - -RT::Interface::Email::Filter::SpamAssassin - Spam filter for RT - -=head1 SYNOPSIS - - # in RT config - Set(@MailPlugins, 'Filter::SpamAssassin', ...other filters...); - -=head1 DESCRIPTION - -This plugin checks to see if an incoming mail is spam (using -C<spamassassin>) and if so, rewrites its headers. If the mail is very -definitely spam - 1.5x more hits than required - then it is dropped on -the floor; otherwise, it is passed on as normal. - -=cut - -RT::Base->_ImportOverlays(); - -1; diff --git a/rt/lib/RT/Interface/REST.pm b/rt/lib/RT/Interface/REST.pm index 7f6c9ac54..aed8f39a2 100644 --- a/rt/lib/RT/Interface/REST.pm +++ b/rt/lib/RT/Interface/REST.pm @@ -2,7 +2,7 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC # <sales@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -46,22 +46,13 @@ # # END BPS TAGGED BLOCK }}} -# lib/RT/Interface/REST.pm -# - package RT::Interface::REST; use strict; use warnings; use RT; -BEGIN { - use base 'Exporter'; - use vars qw($VERSION @EXPORT); - - $VERSION = do { my @r = (q$Revision: 1.1.1.10 $ =~ /\d+/g); sprintf "%d."."%02d"x$#r, @r }; - - @EXPORT = qw(expand_list form_parse form_compose vpush vsplit); -} +use base 'Exporter'; +our @EXPORT = qw(expand_list form_parse form_compose vpush vsplit); sub custom_field_spec { my $self = shift; @@ -200,7 +191,7 @@ sub form_parse { # Returns text representing a set of forms. sub form_compose { my ($forms) = @_; - my (@text, $form); + my (@text); foreach my $form (@$forms) { my ($c, $o, $k, $e) = @$form; @@ -214,10 +205,10 @@ sub form_compose { $text .= $e; } elsif ($o) { - my (@lines, $key); + my (@lines); foreach my $key (@$o) { - my ($line, $sp, $v); + my ($line, $sp); my @values = (ref $k->{$key} eq 'ARRAY') ? @{ $k->{$key} } : $k->{$key}; diff --git a/rt/lib/RT/Interface/Web.pm b/rt/lib/RT/Interface/Web.pm index e4167e4cc..65cf308b2 100644 --- a/rt/lib/RT/Interface/Web.pm +++ b/rt/lib/RT/Interface/Web.pm @@ -2,7 +2,7 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC # <sales@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -65,11 +65,52 @@ package RT::Interface::Web; use RT::SavedSearches; use URI qw(); +use RT::Interface::Web::Menu; use RT::Interface::Web::Session; use Digest::MD5 (); use Encode qw(); +use List::MoreUtils qw(); +use JSON qw(); -# {{{ EscapeUTF8 +=head2 SquishedCSS $style + +=cut + +my %SQUISHED_CSS; +sub SquishedCSS { + my $style = shift or die "need name"; + return $SQUISHED_CSS{$style} if $SQUISHED_CSS{$style}; + require RT::Squish::CSS; + my $css = RT::Squish::CSS->new( Style => $style ); + $SQUISHED_CSS{ $css->Style } = $css; + return $css; +} + +=head2 SquishedJS + +=cut + +my $SQUISHED_JS; +sub SquishedJS { + return $SQUISHED_JS if $SQUISHED_JS; + + require RT::Squish::JS; + my $js = RT::Squish::JS->new(); + $SQUISHED_JS = $js; + return $js; +} + +=head2 ClearSquished + +Removes the cached CSS and JS entries, forcing them to be regenerated +on next use. + +=cut + +sub ClearSquished { + undef $SQUISHED_JS; + %SQUISHED_CSS = (); +} =head2 EscapeUTF8 SCALARREF @@ -90,9 +131,7 @@ sub EscapeUTF8 { $$ref =~ s/'/'/g; } -# }}} -# {{{ EscapeURI =head2 EscapeURI SCALARREF @@ -108,9 +147,16 @@ sub EscapeURI { $$ref =~ s/([^a-zA-Z0-9_.!~*'()-])/uc sprintf("%%%02X", ord($1))/eg; } -# }}} +=head2 EncodeJSON SCALAR + +Encodes the SCALAR to JSON and returns a JSON string. SCALAR may be a simple +value or a reference. -# {{{ WebCanonicalizeInfo +=cut + +sub EncodeJSON { + JSON::to_json(shift, { utf8 => 1, allow_nonref => 1 }); +} =head2 WebCanonicalizeInfo(); @@ -124,9 +170,7 @@ sub WebCanonicalizeInfo { return $ENV{'REMOTE_USER'} ? lc $ENV{'REMOTE_USER'} : $ENV{'REMOTE_USER'}; } -# }}} -# {{{ WebExternalAutoInfo =head2 WebExternalAutoInfo($user); @@ -163,17 +207,21 @@ sub WebExternalAutoInfo { return {%user_info}; } -# }}} sub HandleRequest { my $ARGS = shift; + if (RT->Config->Get('DevelMode')) { + require Module::Refresh; + Module::Refresh->refresh; + } + $HTML::Mason::Commands::r->content_type("text/html; charset=utf-8"); $HTML::Mason::Commands::m->{'rt_base_time'} = [ Time::HiRes::gettimeofday() ]; # Roll back any dangling transactions from a previous failed connection - $RT::Handle->ForceRollback() if $RT::Handle->TransactionDepth; + $RT::Handle->ForceRollback() if $RT::Handle and $RT::Handle->TransactionDepth; MaybeEnableSQLStatementLog(); @@ -183,6 +231,8 @@ sub HandleRequest { $HTML::Mason::Commands::m->autoflush( $HTML::Mason::Commands::m->request_comp->attr('AutoFlush') ) if ( $HTML::Mason::Commands::m->request_comp->attr_exists('AutoFlush') ); + ValidateWebConfig(); + DecodeARGS($ARGS); PreprocessTimeUpdates($ARGS); @@ -243,10 +293,16 @@ sub HandleRequest { $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Default', CallbackPage => '/autohandler' ); ShowRequestedPage($ARGS); - LogRecordedSQLStatements(); + LogRecordedSQLStatements(RequestData => { + Path => $HTML::Mason::Commands::m->request_comp->path, + }); # Process per-page final cleanup callbacks $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Final', CallbackPage => '/autohandler' ); + + $HTML::Mason::Commands::m->comp( '/Elements/Footer', %$ARGS ) + unless $HTML::Mason::Commands::r->content_type + =~ qr<^(text|application)/(x-)?(css|javascript)>; } sub _ForceLogout { @@ -449,6 +505,14 @@ sub MaybeRejectPrivateComponentRequest { return; } +sub InitializeMenu { + $HTML::Mason::Commands::m->notes('menu', RT::Interface::Web::Menu->new()); + $HTML::Mason::Commands::m->notes('page-menu', RT::Interface::Web::Menu->new()); + $HTML::Mason::Commands::m->notes('page-widgets', RT::Interface::Web::Menu->new()); + +} + + =head2 ShowRequestedPage \%ARGS This function, called exclusively by RT's autohandler, dispatches @@ -462,6 +526,11 @@ sub ShowRequestedPage { my $m = $HTML::Mason::Commands::m; + # precache all system level rights for the current user + $HTML::Mason::Commands::session{CurrentUser}->PrincipalObj->HasRights( Object => RT->System ); + + InitializeMenu(); + SendSessionCookie(); # If the user isn't privileged, they can only see SelfService @@ -509,6 +578,7 @@ sub AttemptExternalAuth { $user =~ s/^\Q$NodeName\E\\//i; } + my $next = delete $HTML::Mason::Commands::session{'NextPage'}->{$ARGS->{'next'} || ''}; InstantiateNewSession() unless _UserLoggedIn; $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new(); $HTML::Mason::Commands::session{'CurrentUser'}->$load_method($user); @@ -516,7 +586,7 @@ sub AttemptExternalAuth { if ( RT->Config->Get('WebExternalAuto') and not _UserLoggedIn() ) { # Create users on-the-fly - my $UserObj = RT::User->new($RT::SystemUser); + my $UserObj = RT::User->new(RT->SystemUser); my ( $val, $msg ) = $UserObj->Create( %{ ref RT->Config->Get('AutoCreate') ? RT->Config->Get('AutoCreate') : {} }, Name => $user, @@ -556,6 +626,15 @@ sub AttemptExternalAuth { if ( _UserLoggedIn() ) { $m->callback( %$ARGS, CallbackName => 'ExternalAuthSuccessfulLogin', CallbackPage => '/autohandler' ); + # It is possible that we did a redirect to the login page, + # if the external auth allows lack of auth through with no + # REMOTE_USER set, instead of forcing a "permission + # denied" message. Honor the $next. + Redirect($next) if $next; + # Unlike AttemptPasswordAuthentication below, we do not + # force a redirect to / if $next is not set -- otherwise, + # straight-up external auth would always redirect to / + # when you first hit it. } else { delete $HTML::Mason::Commands::session{'CurrentUser'}; $user = $orig_user; @@ -696,7 +775,7 @@ sub Redirect { # If the user is coming in via a non-canonical # hostname, don't redirect them to the canonical host, # it will just upset them (and invalidate their credentials) - # don't do this if $RT::CanoniaclRedirectURLs is true + # don't do this if $RT::CanonicalizeRedirectURLs is true if ( !RT->Config->Get('CanonicalizeRedirectURLs') && $uri->host eq $server_uri->host && $uri->port eq $server_uri->port ) @@ -708,7 +787,7 @@ sub Redirect { } # [rt3.fsck.com #12716] Apache recommends use of $SERVER_HOST - $uri->host( $ENV{'SERVER_HOST'} || $ENV{'HTTP_HOST'} ); + $uri->host( $ENV{'SERVER_HOST'} || $ENV{'HTTP_HOST'} || $ENV{'SERVER_NAME'}); $uri->port( $ENV{'SERVER_PORT'} ); } @@ -733,7 +812,7 @@ This routine could really use _accurate_ heuristics. (XXX TODO) =cut sub StaticFileHeaders { - my $date = RT::Date->new($RT::SystemUser); + my $date = RT::Date->new(RT->SystemUser); # make cache public $HTML::Mason::Commands::r->headers_out->{'Cache-Control'} = 'max-age=259200, public'; @@ -834,13 +913,6 @@ sub SendStaticFile { } $type ||= "application/octet-stream"; } - - # CGI.pm version 3.51 and 3.52 bang charset=iso-8859-1 onto our JS - # since we don't specify a charset - if ( $type =~ m{application/javascript} && - $type !~ m{charset=([\w-]+)$} ) { - $type .= "; charset=utf-8"; - } $HTML::Mason::Commands::r->content_type($type); open( my $fh, '<', $file ) or die "couldn't open file: $!"; binmode($fh); @@ -852,6 +924,21 @@ sub SendStaticFile { close $fh; } + + +sub MobileClient { + my $self = shift; + + +if (($ENV{'HTTP_USER_AGENT'} || '') =~ /(?:hiptop|Blazer|Novarra|Vagabond|SonyEricsson|Symbian|NetFront|UP.Browser|UP.Link|Windows CE|MIDP|J2ME|DoCoMo|J-PHONE|PalmOS|PalmSource|iPhone|iPod|AvantGo|Nokia|Android|WebOS|S60)/io && !$HTML::Mason::Commands::session{'NotMobile'}) { + return 1; +} else { + return undef; +} + +} + + sub StripContent { my %args = @_; my $content = $args{Content}; @@ -887,15 +974,14 @@ sub StripContent { return '' if not $html and $content =~ /^(--)?\Q$sig\E$/; # Check for html-formatted sig; we don't use EscapeUTF8 here - # because we want to precisely match the escaping that FCKEditor - # uses. see also 311223f5, which fixed this for 4.0 + # because we want to precisely match the escapting that FCKEditor + # uses. $sig =~ s/&/&/g; $sig =~ s/</</g; $sig =~ s/>/>/g; - - return '' - if $html - and $content =~ m{^(?:<p>)?(--)?\Q$sig\E(?:</p>)?$}s; + $sig =~ s/"/"/g; + $sig =~ s/'/'/g; + return '' if $html and $content =~ m{^(?:<p>)?(--)?\Q$sig\E(?:</p>)?$}s; # Pass it through return $return_content; @@ -963,12 +1049,20 @@ sub MaybeEnableSQLStatementLog { } sub LogRecordedSQLStatements { + my %args = @_; + my $log_sql_statements = RT->Config->Get('StatementLog'); return unless ($log_sql_statements); my @log = $RT::Handle->SQLStatementLog; $RT::Handle->ClearSQLStatementLog; + + $RT::Handle->AddRequestToHistory({ + %{ $args{RequestData} }, + Queries => \@log, + }); + for my $stmt (@log) { my ( $time, $sql, $bind, $duration ) = @{$stmt}; my @bind; @@ -984,17 +1078,77 @@ sub LogRecordedSQLStatements { message => "SQL(" . sprintf( "%.6f", $duration ) . "s): $sql;" - . ( @bind ? " [ bound values: @{[map{qq|'$_'|} @bind]} ]" : "" ) + . ( @bind ? " [ bound values: @{[map{ defined $_ ? qq|'$_'| : 'undef'} @bind]} ]" : "" ) ); } } +my $_has_validated_web_config = 0; +sub ValidateWebConfig { + my $self = shift; + + # do this once per server instance, not once per request + return if $_has_validated_web_config; + $_has_validated_web_config = 1; + + if (!$ENV{'rt.explicit_port'} && $ENV{SERVER_PORT} != RT->Config->Get('WebPort')) { + $RT::Logger->warn("The actual SERVER_PORT ($ENV{SERVER_PORT}) does NOT match the configured WebPort ($RT::WebPort). Perhaps you should Set(\$WebPort, $ENV{SERVER_PORT}); in RT_SiteConfig.pm, otherwise your internal links may be broken."); + } + + if ($ENV{HTTP_HOST}) { + # match "example.com" or "example.com:80" + my ($host) = $ENV{HTTP_HOST} =~ /^(.*?)(:\d+)?$/; + + if ($host ne RT->Config->Get('WebDomain')) { + $RT::Logger->warn("The actual HTTP_HOST ($host) does NOT match the configured WebDomain ($RT::WebDomain). Perhaps you should Set(\$WebDomain, '$host'); in RT_SiteConfig.pm, otherwise your internal links may be broken."); + } + } + else { + if ($ENV{SERVER_NAME} ne RT->Config->Get('WebDomain')) { + $RT::Logger->warn("The actual SERVER_NAME ($ENV{SERVER_NAME}) does NOT match the configured WebDomain ($RT::WebDomain). Perhaps you should Set(\$WebDomain, '$ENV{SERVER_NAME}'); in RT_SiteConfig.pm, otherwise your internal links may be broken."); + } + } + + if ($ENV{SCRIPT_NAME} ne RT->Config->Get('WebPath')) { + $RT::Logger->warn("The actual SCRIPT_NAME ($ENV{SCRIPT_NAME}) does NOT match the configured WebPath ($RT::WebPath). Perhaps you should Set(\$WebPath, '$ENV{SCRIPT_NAME}'); in RT_SiteConfig.pm, otherwise your internal links may be broken."); + } +} + +sub ComponentRoots { + my $self = shift; + my %args = ( Names => 0, @_ ); + my @roots; + if (defined $HTML::Mason::Commands::m) { + @roots = $HTML::Mason::Commands::m->interp->comp_root_array; + } else { + @roots = ( + [ local => $RT::MasonLocalComponentRoot ], + (map {[ "plugin-".$_->Name => $_->ComponentRoot ]} @{RT->Plugins}), + [ standard => $RT::MasonComponentRoot ] + ); + } + @roots = map { $_->[1] } @roots unless $args{Names}; + return @roots; +} + package HTML::Mason::Commands; use vars qw/$r $m %session/; -# {{{ loc +sub Menu { + return $HTML::Mason::Commands::m->notes('menu'); +} + +sub PageMenu { + return $HTML::Mason::Commands::m->notes('page-menu'); +} + +sub PageWidgets { + return $HTML::Mason::Commands::m->notes('page-widgets'); +} + + =head2 loc ARRAY @@ -1025,9 +1179,7 @@ sub loc { } } -# }}} -# {{{ loc_fuzzy =head2 loc_fuzzy STRING @@ -1047,14 +1199,12 @@ sub loc_fuzzy { { return ( $session{'CurrentUser'}->loc_fuzzy($msg) ); } else { - my $u = RT::CurrentUser->new( $RT::SystemUser->Id ); + my $u = RT::CurrentUser->new( RT->SystemUser->Id ); return ( $u->loc_fuzzy($msg) ); } } -# }}} -# {{{ sub Abort # Error - calls Error and aborts sub Abort { my $why = shift; @@ -1072,9 +1222,79 @@ sub Abort { } } -# }}} +sub MaybeRedirectForResults { + my %args = ( + Path => $HTML::Mason::Commands::m->request_comp->path, + Arguments => {}, + Anchor => undef, + Actions => undef, + Force => 0, + @_ + ); + my $has_actions = $args{'Actions'} && grep( defined, @{ $args{'Actions'} } ); + return unless $has_actions || $args{'Force'}; + + my %arguments = %{ $args{'Arguments'} }; + + if ( $has_actions ) { + my $key = Digest::MD5::md5_hex( rand(1024) ); + push @{ $session{"Actions"}{ $key } ||= [] }, @{ $args{'Actions'} }; + $session{'i'}++; + $arguments{'results'} = $key; + } + + $args{'Path'} =~ s!^/+!!; + my $url = RT->Config->Get('WebURL') . $args{Path}; + + if ( keys %arguments ) { + $url .= '?'. $m->comp( '/Elements/QueryString', %arguments ); + } + if ( $args{'Anchor'} ) { + $url .= "#". $args{'Anchor'}; + } + return RT::Interface::Web::Redirect($url); +} + +=head2 MaybeRedirectToApproval Path => 'path', Whitelist => REGEX, ARGSRef => HASHREF + +If the ticket specified by C<< $ARGSRef->{id} >> is an approval ticket, +redirect to the approvals display page, preserving any arguments. + +C<Path>s matching C<Whitelist> are let through. + +This is a no-op if the C<ForceApprovalsView> option isn't enabled. -# {{{ sub CreateTicket +=cut + +sub MaybeRedirectToApproval { + my %args = ( + Path => $HTML::Mason::Commands::m->request_comp->path, + ARGSRef => {}, + Whitelist => undef, + @_ + ); + + return unless $ENV{REQUEST_METHOD} eq 'GET'; + + my $id = $args{ARGSRef}->{id}; + + if ( $id + and RT->Config->Get('ForceApprovalsView') + and not $args{Path} =~ /$args{Whitelist}/) + { + my $ticket = RT::Ticket->new( $session{'CurrentUser'} ); + $ticket->Load($id); + + if ($ticket and $ticket->id and lc($ticket->Type) eq 'approval') { + MaybeRedirectForResults( + Path => "/Approvals/Display.html", + Force => 1, + Anchor => $args{ARGSRef}->{Anchor}, + Arguments => $args{ARGSRef}, + ); + } + } +} =head2 CreateTicket ARGS @@ -1087,9 +1307,9 @@ sub CreateTicket { my (@Actions); - my $Ticket = new RT::Ticket( $session{'CurrentUser'} ); + my $Ticket = RT::Ticket->new( $session{'CurrentUser'} ); - my $Queue = new RT::Queue( $session{'CurrentUser'} ); + my $Queue = RT::Queue->new( $session{'CurrentUser'} ); unless ( $Queue->Load( $ARGS{'Queue'} ) ) { Abort('Queue not found'); } @@ -1100,12 +1320,12 @@ sub CreateTicket { my $due; if ( defined $ARGS{'Due'} and $ARGS{'Due'} =~ /\S/ ) { - $due = new RT::Date( $session{'CurrentUser'} ); + $due = RT::Date->new( $session{'CurrentUser'} ); $due->Set( Format => 'unknown', Value => $ARGS{'Due'} ); } my $starts; if ( defined $ARGS{'Starts'} and $ARGS{'Starts'} =~ /\S/ ) { - $starts = new RT::Date( $session{'CurrentUser'} ); + $starts = RT::Date->new( $session{'CurrentUser'} ); $starts->Set( Format => 'unknown', Value => $ARGS{'Starts'} ); } @@ -1139,9 +1359,8 @@ sub CreateTicket { } foreach my $argument (qw(Encrypt Sign)) { - $MIMEObj->head->add( - "X-RT-$argument" => Encode::encode_utf8( $ARGS{$argument} ) - ) if defined $ARGS{$argument}; + $MIMEObj->head->replace( "X-RT-$argument" => $ARGS{$argument} ? 1 : 0 ) + if defined $ARGS{$argument}; } my %create_args = ( @@ -1165,17 +1384,13 @@ sub CreateTicket { MIMEObj => $MIMEObj ); - my @temp_squelch; + my @txn_squelch; foreach my $type (qw(Requestor Cc AdminCc)) { - push @temp_squelch, map $_->address, Email::Address->parse( $create_args{$type} ) + push @txn_squelch, map $_->address, Email::Address->parse( $create_args{$type} ) if grep $_ eq $type || $_ eq ( $type . 's' ), @{ $ARGS{'SkipNotification'} || [] }; - - } - - if (@temp_squelch) { - require RT::Action::SendEmail; - RT::Action::SendEmail->SquelchMailTo( RT::Action::SendEmail->SquelchMailTo, @temp_squelch ); } + $create_args{TransSquelchMailTo} = \@txn_squelch + if @txn_squelch; if ( $ARGS{'AttachTickets'} ) { require RT::Action::SendEmail; @@ -1259,9 +1474,7 @@ sub CreateTicket { } -# }}} -# {{{ sub LoadTicket - loads a ticket =head2 LoadTicket id @@ -1291,9 +1504,7 @@ sub LoadTicket { return $Ticket; } -# }}} -# {{{ sub ProcessUpdateMessage =head2 ProcessUpdateMessage @@ -1341,7 +1552,7 @@ sub ProcessUpdateMessage { return; } - if ( $args{ARGSRef}->{'UpdateSubject'} eq $args{'TicketObj'}->Subject ) { + if ( $args{ARGSRef}->{'UpdateSubject'} eq ($args{'TicketObj'}->Subject || '') ) { $args{ARGSRef}->{'UpdateSubject'} = undef; } @@ -1351,7 +1562,7 @@ sub ProcessUpdateMessage { Type => $args{ARGSRef}->{'UpdateContentType'}, ); - $Message->head->add( 'Message-ID' => Encode::encode_utf8( + $Message->head->replace( 'Message-ID' => Encode::encode_utf8( RT::Interface::Email::GenMessageId( Ticket => $args{'TicketObj'} ) ) ); my $old_txn = RT::Transaction->new( $session{'CurrentUser'} ); @@ -1381,9 +1592,6 @@ sub ProcessUpdateMessage { : ( $args{ARGSRef}->{'AttachTickets'} ) ); } - my $bcc = $args{ARGSRef}->{'UpdateBcc'}; - my $cc = $args{ARGSRef}->{'UpdateCc'}; - my %txn_customfields; foreach my $key ( keys %{ $args{ARGSRef} } ) { @@ -1393,70 +1601,83 @@ sub ProcessUpdateMessage { } my %message_args = ( - CcMessageTo => $cc, - BccMessageTo => $bcc, - Sign => $args{ARGSRef}->{'Sign'}, - Encrypt => $args{ARGSRef}->{'Encrypt'}, + Sign => ( $args{ARGSRef}->{'Sign'} ? 1 : 0 ), + Encrypt => ( $args{ARGSRef}->{'Encrypt'} ? 1 : 0 ), MIMEObj => $Message, TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'}, CustomFields => \%txn_customfields, ); - my @temp_squelch; + _ProcessUpdateMessageRecipients( + MessageArgs => \%message_args, + %args, + ); + + my @results; + if ( $args{ARGSRef}->{'UpdateType'} =~ /^(private|public)$/ ) { + my ( $Transaction, $Description, $Object ) = $args{TicketObj}->Comment(%message_args); + push( @results, $Description ); + $Object->UpdateCustomFields( ARGSRef => $args{ARGSRef} ) if $Object; + } elsif ( $args{ARGSRef}->{'UpdateType'} eq 'response' ) { + my ( $Transaction, $Description, $Object ) = $args{TicketObj}->Correspond(%message_args); + push( @results, $Description ); + $Object->UpdateCustomFields( ARGSRef => $args{ARGSRef} ) if $Object; + } else { + push( @results, + loc("Update type was neither correspondence nor comment.") . " " . loc("Update not recorded.") ); + } + return @results; +} + +sub _ProcessUpdateMessageRecipients { + my %args = ( + ARGSRef => undef, + TicketObj => undef, + MessageArgs => undef, + @_, + ); + + my $bcc = $args{ARGSRef}->{'UpdateBcc'}; + my $cc = $args{ARGSRef}->{'UpdateCc'}; + + my $message_args = $args{MessageArgs}; + + $message_args->{CcMessageTo} = $cc; + $message_args->{BccMessageTo} = $bcc; + + my @txn_squelch; foreach my $type (qw(Cc AdminCc)) { if (grep $_ eq $type || $_ eq ( $type . 's' ), @{ $args{ARGSRef}->{'SkipNotification'} || [] }) { - push @temp_squelch, map $_->address, Email::Address->parse( $message_args{$type} ); - push @temp_squelch, $args{TicketObj}->$type->MemberEmailAddresses; - push @temp_squelch, $args{TicketObj}->QueueObj->$type->MemberEmailAddresses; + push @txn_squelch, map $_->address, Email::Address->parse( $message_args->{$type} ); + push @txn_squelch, $args{TicketObj}->$type->MemberEmailAddresses; + push @txn_squelch, $args{TicketObj}->QueueObj->$type->MemberEmailAddresses; } } if (grep $_ eq 'Requestor' || $_ eq 'Requestors', @{ $args{ARGSRef}->{'SkipNotification'} || [] }) { - push @temp_squelch, map $_->address, Email::Address->parse( $message_args{Requestor} ); - push @temp_squelch, $args{TicketObj}->Requestors->MemberEmailAddresses; - } + push @txn_squelch, map $_->address, Email::Address->parse( $message_args->{Requestor} ); + push @txn_squelch, $args{TicketObj}->Requestors->MemberEmailAddresses; - if (@temp_squelch) { - require RT::Action::SendEmail; - RT::Action::SendEmail->SquelchMailTo( RT::Action::SendEmail->SquelchMailTo, @temp_squelch ); } + push @txn_squelch, @{$args{ARGSRef}{SquelchMailTo}} if $args{ARGSRef}{SquelchMailTo}; + $message_args->{SquelchMailTo} = \@txn_squelch + if @txn_squelch; + unless ( $args{'ARGSRef'}->{'UpdateIgnoreAddressCheckboxes'} ) { foreach my $key ( keys %{ $args{ARGSRef} } ) { next unless $key =~ /^Update(Cc|Bcc)-(.*)$/; my $var = ucfirst($1) . 'MessageTo'; my $value = $2; - if ( $message_args{$var} ) { - $message_args{$var} .= ", $value"; + if ( $message_args->{$var} ) { + $message_args->{$var} .= ", $value"; } else { - $message_args{$var} = $value; + $message_args->{$var} = $value; } } } - - my @results; - # Do the update via the appropriate Ticket method - if ( $args{ARGSRef}->{'UpdateType'} =~ /^(private|public)$/ ) { - my ( $Transaction, $Description, $Object ) = - $args{TicketObj}->Comment(%message_args); - push( @results, $Description ); - #$Object->UpdateCustomFields( ARGSRef => $args{ARGSRef} ) if $Object; - } elsif ( $args{ARGSRef}->{'UpdateType'} eq 'response' ) { - my ( $Transaction, $Description, $Object ) = - $args{TicketObj}->Correspond(%message_args); - push( @results, $Description ); - #$Object->UpdateCustomFields( ARGSRef => $args{ARGSRef} ) if $Object; - } else { - push( @results, - loc("Update type was neither correspondence nor comment.") . " " . loc("Update not recorded.") ); - } - return @results; } -# }}} - -# {{{ sub MakeMIMEEntity - =head2 MakeMIMEEntity PARAMHASH Takes a paramhash Subject, Body and AttachmentFieldName. @@ -1481,6 +1702,7 @@ sub MakeMIMEEntity { ); my $Message = MIME::Entity->build( Type => 'multipart/mixed', + "Message-Id" => RT::Interface::Email::GenMessageId, map { $_ => Encode::encode_utf8( $args{ $_} ) } grep defined $args{$_}, qw(Subject From Cc) ); @@ -1500,8 +1722,8 @@ sub MakeMIMEEntity { if ( $args{'AttachmentFieldName'} ) { my $cgi_object = $m->cgi_object; - - if ( my $filehandle = $cgi_object->upload( $args{'AttachmentFieldName'} ) ) { + my $filehandle = $cgi_object->upload( $args{'AttachmentFieldName'} ); + if ( defined $filehandle && length $filehandle ) { my ( @content, $buffer ); while ( my $bytesread = read( $filehandle, $buffer, 4096 ) ) { @@ -1510,10 +1732,7 @@ sub MakeMIMEEntity { my $uploadinfo = $cgi_object->uploadInfo($filehandle); - # Prefer the cached name first over CGI.pm stringification. - my $filename = $RT::Mason::CGI::Filename; - $filename = "$filehandle" unless defined $filename; - $filename = Encode::encode_utf8( $filename ); + my $filename = "$filehandle"; $filename =~ s{^.*[\\/]}{}; $Message->attach( @@ -1524,6 +1743,9 @@ sub MakeMIMEEntity { if ( !$args{'Subject'} && !( defined $args{'Body'} && length $args{'Body'} ) ) { $Message->head->set( 'Subject' => $filename ); } + + # Attachment parts really shouldn't get a Message-ID + $Message->head->delete('Message-ID'); } } @@ -1535,9 +1757,7 @@ sub MakeMIMEEntity { } -# }}} -# {{{ sub ParseDateToISO =head2 ParseDateToISO @@ -1557,9 +1777,7 @@ sub ParseDateToISO { return ( $date_obj->ISO ); } -# }}} -# {{{ sub ProcessACLChanges sub ProcessACLChanges { my $ARGSref = shift; @@ -1608,9 +1826,145 @@ sub ProcessACLChanges { return (@results); } -# }}} -# {{{ sub UpdateRecordObj +=head2 ProcessACLs + +ProcessACLs expects values from a series of checkboxes that describe the full +set of rights a principal should have on an object. + +It expects form inputs with names like SetRights-PrincipalId-ObjType-ObjId +instead of with the prefixes Grant/RevokeRight. Each input should be an array +listing the rights the principal should have, and ProcessACLs will modify the +current rights to match. Additionally, the previously unused CheckACL input +listing PrincipalId-ObjType-ObjId is now used to catch cases when all the +rights are removed from a principal and as such no SetRights input is +submitted. + +=cut + +sub ProcessACLs { + my $ARGSref = shift; + my (%state, @results); + + my $CheckACL = $ARGSref->{'CheckACL'}; + my @check = grep { defined } (ref $CheckACL eq 'ARRAY' ? @$CheckACL : $CheckACL); + + # Check if we want to grant rights to a previously rights-less user + for my $type (qw(user group)) { + my $key = "AddPrincipalForRights-$type"; + + next unless $ARGSref->{$key}; + + my $principal; + if ( $type eq 'user' ) { + $principal = RT::User->new( $session{'CurrentUser'} ); + $principal->LoadByCol( Name => $ARGSref->{$key} ); + } + else { + $principal = RT::Group->new( $session{'CurrentUser'} ); + $principal->LoadUserDefinedGroup( $ARGSref->{$key} ); + } + + unless ($principal->PrincipalId) { + push @results, loc("Couldn't load the specified principal"); + next; + } + + my $principal_id = $principal->PrincipalId; + + # Turn our addprincipal rights spec into a real one + for my $arg (keys %$ARGSref) { + next unless $arg =~ /^SetRights-addprincipal-(.+?-\d+)$/; + + my $tuple = "$principal_id-$1"; + my $key = "SetRights-$tuple"; + + # If we have it already, that's odd, but merge them + if (grep { $_ eq $tuple } @check) { + $ARGSref->{$key} = [ + (ref $ARGSref->{$key} eq 'ARRAY' ? @{$ARGSref->{$key}} : $ARGSref->{$key}), + (ref $ARGSref->{$arg} eq 'ARRAY' ? @{$ARGSref->{$arg}} : $ARGSref->{$arg}), + ]; + } else { + $ARGSref->{$key} = $ARGSref->{$arg}; + push @check, $tuple; + } + } + } + + # Build our rights state for each Principal-Object tuple + foreach my $arg ( keys %$ARGSref ) { + next unless $arg =~ /^SetRights-(\d+-.+?-\d+)$/; + + my $tuple = $1; + my $value = $ARGSref->{$arg}; + my @rights = grep { $_ } (ref $value eq 'ARRAY' ? @$value : $value); + next unless @rights; + + $state{$tuple} = { map { $_ => 1 } @rights }; + } + + foreach my $tuple (List::MoreUtils::uniq @check) { + next unless $tuple =~ /^(\d+)-(.+?)-(\d+)$/; + + my ( $principal_id, $object_type, $object_id ) = ( $1, $2, $3 ); + + my $principal = RT::Principal->new( $session{'CurrentUser'} ); + $principal->Load($principal_id); + + my $obj; + if ( $object_type eq 'RT::System' ) { + $obj = $RT::System; + } elsif ( $RT::ACE::OBJECT_TYPES{$object_type} ) { + $obj = $object_type->new( $session{'CurrentUser'} ); + $obj->Load($object_id); + unless ( $obj->id ) { + $RT::Logger->error("couldn't load $object_type #$object_id"); + next; + } + } else { + $RT::Logger->error("object type '$object_type' is incorrect"); + push( @results, loc("System Error") . ': ' . loc( "Rights could not be granted for [_1]", $object_type ) ); + next; + } + + my $acls = RT::ACL->new($session{'CurrentUser'}); + $acls->LimitToObject( $obj ); + $acls->LimitToPrincipal( Id => $principal_id ); + + while ( my $ace = $acls->Next ) { + my $right = $ace->RightName; + + # Has right and should have right + next if delete $state{$tuple}->{$right}; + + # Has right and shouldn't have right + my ($val, $msg) = $principal->RevokeRight( Object => $obj, Right => $right ); + push @results, $msg; + } + + # For everything left, they don't have the right but they should + for my $right (keys %{ $state{$tuple} || {} }) { + delete $state{$tuple}->{$right}; + my ($val, $msg) = $principal->GrantRight( Object => $obj, Right => $right ); + push @results, $msg; + } + + # Check our state for leftovers + if ( keys %{ $state{$tuple} || {} } ) { + my $missed = join '|', %{$state{$tuple} || {}}; + $RT::Logger->warn( + "Uh-oh, it looks like we somehow missed a right in " + ."ProcessACLs. Here's what was leftover: $missed" + ); + } + } + + return (@results); +} + + + =head2 UpdateRecordObj ( ARGSRef => \%ARGS, Object => RT::Record, AttributesRef => \@attribs) @@ -1639,9 +1993,7 @@ sub UpdateRecordObject { return (@results); } -# }}} -# {{{ Sub ProcessCustomFieldUpdates sub ProcessCustomFieldUpdates { my %args = ( @@ -1694,9 +2046,7 @@ sub ProcessCustomFieldUpdates { return (@results); } -# }}} -# {{{ sub ProcessTicketBasics =head2 ProcessTicketBasics ( TicketObj => $Ticket, ARGSRef => \%ARGS ); @@ -1715,7 +2065,9 @@ sub ProcessTicketBasics { my $TicketObj = $args{'TicketObj'}; my $ARGSRef = $args{'ARGSRef'}; - # {{{ Set basic fields + my $OrigOwner = $TicketObj->Owner; + + # Set basic fields my @attribs = qw( Subject FinalPriority @@ -1728,11 +2080,15 @@ sub ProcessTicketBasics { Queue ); - if ( $ARGSRef->{'Queue'} and ( $ARGSRef->{'Queue'} !~ /^(\d+)$/ ) ) { - my $tempqueue = RT::Queue->new($RT::SystemUser); - $tempqueue->Load( $ARGSRef->{'Queue'} ); - if ( $tempqueue->id ) { - $ARGSRef->{'Queue'} = $tempqueue->id; + # Canonicalize Queue and Owner to their IDs if they aren't numeric + for my $field (qw(Queue Owner)) { + if ( $ARGSRef->{$field} and ( $ARGSRef->{$field} !~ /^(\d+)$/ ) ) { + my $class = $field eq 'Owner' ? "RT::User" : "RT::$field"; + my $temp = $class->new(RT->SystemUser); + $temp->Load( $ARGSRef->{$field} ); + if ( $temp->id ) { + $ARGSRef->{$field} = $temp->id; + } } } @@ -1747,12 +2103,15 @@ sub ProcessTicketBasics { ); # We special case owner changing, so we can use ForceOwnerChange - if ( $ARGSRef->{'Owner'} && ( $TicketObj->Owner != $ARGSRef->{'Owner'} ) ) { + if ( $ARGSRef->{'Owner'} + && $ARGSRef->{'Owner'} !~ /\D/ + && ( $OrigOwner != $ARGSRef->{'Owner'} ) ) { my ($ChownType); if ( $ARGSRef->{'ForceOwnerChange'} ) { $ChownType = "Force"; - } else { - $ChownType = "Give"; + } + else { + $ChownType = "Set"; } my ( $val, $msg ) = $TicketObj->SetOwner( $ARGSRef->{'Owner'}, $ChownType ); @@ -1764,7 +2123,64 @@ sub ProcessTicketBasics { return (@results); } -# }}} +sub ProcessTicketReminders { + my %args = ( + TicketObj => undef, + ARGSRef => undef, + @_ + ); + + my $Ticket = $args{'TicketObj'}; + my $args = $args{'ARGSRef'}; + my @results; + + my $reminder_collection = $Ticket->Reminders->Collection; + + if ( $args->{'update-reminders'} ) { + while ( my $reminder = $reminder_collection->Next ) { + if ( $reminder->Status ne 'resolved' && $args->{ 'Complete-Reminder-' . $reminder->id } ) { + $Ticket->Reminders->Resolve($reminder); + } + elsif ( $reminder->Status eq 'resolved' && !$args->{ 'Complete-Reminder-' . $reminder->id } ) { + $Ticket->Reminders->Open($reminder); + } + + if ( exists( $args->{ 'Reminder-Subject-' . $reminder->id } ) && ( $reminder->Subject ne $args->{ 'Reminder-Subject-' . $reminder->id } )) { + $reminder->SetSubject( $args->{ 'Reminder-Subject-' . $reminder->id } ) ; + } + + if ( exists( $args->{ 'Reminder-Owner-' . $reminder->id } ) && ( $reminder->Owner != $args->{ 'Reminder-Owner-' . $reminder->id } )) { + $reminder->SetOwner( $args->{ 'Reminder-Owner-' . $reminder->id } , "Force" ) ; + } + + if ( exists( $args->{ 'Reminder-Due-' . $reminder->id } ) && $args->{ 'Reminder-Due-' . $reminder->id } ne '' ) { + my $DateObj = RT::Date->new( $session{'CurrentUser'} ); + $DateObj->Set( + Format => 'unknown', + Value => $args->{ 'Reminder-Due-' . $reminder->id } + ); + if ( defined $DateObj->Unix && $DateObj->Unix != $reminder->DueObj->Unix ) { + $reminder->SetDue( $DateObj->ISO ); + } + } + } + } + + if ( $args->{'NewReminder-Subject'} ) { + my $due_obj = RT::Date->new( $session{'CurrentUser'} ); + $due_obj->Set( + Format => 'unknown', + Value => $args->{'NewReminder-Due'} + ); + my ( $add_id, $msg, $txnid ) = $Ticket->Reminders->Add( + Subject => $args->{'NewReminder-Subject'}, + Owner => $args->{'NewReminder-Owner'}, + Due => $due_obj->ISO + ); + push @results, loc("Reminder '[_1]' added", $args->{'NewReminder-Subject'}); + } + return @results; +} sub ProcessTicketCustomFieldUpdates { my %args = @_; @@ -1838,7 +2254,7 @@ sub ProcessObjectCustomFieldUpdates { sub _ProcessObjectCustomFieldUpdates { my %args = @_; my $cf = $args{'CustomField'}; - my $cf_type = $cf->Type; + my $cf_type = $cf->Type || ''; # Remove blank Values since the magic field will take care of this. Sometimes # the browser gives you a blank value which causes CFs to be processed twice @@ -1939,6 +2355,9 @@ sub _ProcessObjectCustomFieldUpdates { # For Date Cfs, @values is empty when there is no changes (no datas in form input) return @results if ( $cf->Type eq 'Date' && ! @values ); + # For Date Cfs, @values is empty when there is no changes (no datas in form input) + return @results if ( $cf->Type =~ /^Date(?:Time)?$/ && ! @values ); + $cf_values->RedoSearch; while ( my $cf_value = $cf_values->Next ) { next if $values_hash{ $cf_value->id }; @@ -1985,7 +2404,6 @@ sub _ProcessObjectCustomFieldUpdates { return @results; } -# {{{ sub ProcessTicketWatchers =head2 ProcessTicketWatchers ( TicketObj => $Ticket, ARGSRef => \%ARGS ); @@ -2067,9 +2485,7 @@ sub ProcessTicketWatchers { return (@results); } -# }}} -# {{{ sub ProcessTicketDates =head2 ProcessTicketDates ( TicketObj => $Ticket, ARGSRef => \%ARGS ); @@ -2089,7 +2505,7 @@ sub ProcessTicketDates { my (@results); - # {{{ Set date fields + # Set date fields my @date_fields = qw( Told Resolved @@ -2125,9 +2541,7 @@ sub ProcessTicketDates { return (@results); } -# }}} -# {{{ sub ProcessTicketLinks =head2 ProcessTicketLinks ( TicketObj => $Ticket, ARGSRef => \%ARGS ); @@ -2157,7 +2571,6 @@ sub ProcessTicketLinks { return (@results); } -# }}} sub ProcessRecordLinks { my %args = ( @@ -2291,6 +2704,99 @@ sub ProcessColumnMapValue { return $value; } +=head2 GetPrincipalsMap OBJECT, CATEGORIES + +Returns an array suitable for passing to /Admin/Elements/EditRights with the +principal collections mapped from the categories given. + +=cut + +sub GetPrincipalsMap { + my $object = shift; + my @map; + for (@_) { + if (/System/) { + my $system = RT::Groups->new($session{'CurrentUser'}); + $system->LimitToSystemInternalGroups(); + $system->OrderBy( FIELD => 'Type', ORDER => 'ASC' ); + push @map, [ + 'System' => $system, # loc_left_pair + 'Type' => 1, + ]; + } + elsif (/Groups/) { + my $groups = RT::Groups->new($session{'CurrentUser'}); + $groups->LimitToUserDefinedGroups(); + $groups->OrderBy( FIELD => 'Name', ORDER => 'ASC' ); + + # Only show groups who have rights granted on this object + $groups->WithGroupRight( + Right => '', + Object => $object, + IncludeSystemRights => 0, + IncludeSubgroupMembers => 0, + ); + + push @map, [ + 'User Groups' => $groups, # loc_left_pair + 'Name' => 0 + ]; + } + elsif (/Roles/) { + my $roles = RT::Groups->new($session{'CurrentUser'}); + + if ($object->isa('RT::System')) { + $roles->LimitToRolesForSystem(); + } + elsif ($object->isa('RT::Queue')) { + $roles->LimitToRolesForQueue($object->Id); + } + else { + $RT::Logger->warn("Skipping unknown object type ($object) for Role principals"); + next; + } + $roles->OrderBy( FIELD => 'Type', ORDER => 'ASC' ); + push @map, [ + 'Roles' => $roles, # loc_left_pair + 'Type' => 1 + ]; + } + elsif (/Users/) { + my $Users = RT->PrivilegedUsers->UserMembersObj(); + $Users->OrderBy( FIELD => 'Name', ORDER => 'ASC' ); + + # Only show users who have rights granted on this object + my $group_members = $Users->WhoHaveGroupRight( + Right => '', + Object => $object, + IncludeSystemRights => 0, + IncludeSubgroupMembers => 0, + ); + + # Limit to UserEquiv groups + my $groups = $Users->NewAlias('Groups'); + $Users->Join( + ALIAS1 => $groups, + FIELD1 => 'id', + ALIAS2 => $group_members, + FIELD2 => 'GroupId' + ); + $Users->Limit( ALIAS => $groups, FIELD => 'Domain', VALUE => 'ACLEquivalence' ); + $Users->Limit( ALIAS => $groups, FIELD => 'Type', VALUE => 'UserEquiv' ); + + + my $display = sub { + $m->scomp('/Elements/ShowUser', User => $_[0], NoEscape => 1) + }; + push @map, [ + 'Users' => $Users, # loc_left_pair + $display => 0 + ]; + } + } + return @map; +} + =head2 _load_container_object ( $type, $id ); Instantiate container object for saving searches. @@ -2322,6 +2828,82 @@ sub _parse_saved_search { return ( _load_container_object( $obj_type, $obj_id ), $search_id ); } +=head2 ScrubHTML content + +Removes unsafe and undesired HTML from the passed content + +=cut + +my $SCRUBBER; +sub ScrubHTML { + my $Content = shift; + $SCRUBBER = _NewScrubber() unless $SCRUBBER; + + $Content = '' if !defined($Content); + return $SCRUBBER->scrub($Content); +} + +=head2 _NewScrubber + +Returns a new L<HTML::Scrubber> object. Override this if you insist on +letting more HTML through. + +=cut + +sub _NewScrubber { + require HTML::Scrubber; + my $scrubber = HTML::Scrubber->new(); + $scrubber->default( + 0, + { + '*' => 0, + id => 1, + class => 1, + # Match http, ftp and relative urls + # XXX: we also scrub format strings with this module then allow simple config options + href => qr{^(?:http:|ftp:|https:|/|__Web(?:Path|BaseURL|URL)__)}i, + face => 1, + size => 1, + target => 1, + style => qr{ + ^(?:\s* + (?:(?:background-)?color: \s* + (?:rgb\(\s* \d+, \s* \d+, \s* \d+ \s*\) | # rgb(d,d,d) + \#[a-f0-9]{3,6} | # #fff or #ffffff + [\w\-]+ # green, light-blue, etc. + ) | + text-align: \s* \w+ | + font-size: \s* [\w.\-]+ | + font-family: \s* [\w\s"',.\-]+ | + font-weight: \s* [\w\-]+ | + + # MS Office styles, which are probably fine. If we don't, then any + # associated styles in the same attribute get stripped. + mso-[\w\-]+?: \s* [\w\s"',.\-]+ + )\s* ;? \s*) + +$ # one or more of these allowed properties from here 'till sunset + }ix, + } + ); + $scrubber->deny(qw[*]); + $scrubber->allow( + qw[A B U P BR I HR BR SMALL EM FONT SPAN STRONG SUB SUP STRIKE H1 H2 H3 H4 H5 H6 DIV UL OL LI DL DT DD PRE BLOCKQUOTE] + ); + $scrubber->comment(0); + + return $scrubber; +} + +=head2 JSON + +Redispatches to L<RT::Interface::Web/EncodeJSON> + +=cut + +sub JSON { + RT::Interface::Web::EncodeJSON(@_); +} + package RT::Interface::Web; RT::Base->_ImportOverlays(); diff --git a/rt/lib/RT/Interface/Web/Handler.pm b/rt/lib/RT/Interface/Web/Handler.pm index 4bb648451..69eee60f6 100644 --- a/rt/lib/RT/Interface/Web/Handler.pm +++ b/rt/lib/RT/Interface/Web/Handler.pm @@ -2,7 +2,7 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC # <sales@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -47,6 +47,8 @@ # END BPS TAGGED BLOCK }}} package RT::Interface::Web::Handler; +use warnings; +use strict; use CGI qw/-private_tempfiles/; use MIME::Entity; @@ -54,19 +56,16 @@ use Text::Wrapper; use CGI::Cookie; use Time::ParseDate; use Time::HiRes; -use HTML::Entities; use HTML::Scrubber; -use RT::Interface::Web::Handler; +use RT::Interface::Web; use RT::Interface::Web::Request; use File::Path qw( rmtree ); use File::Glob qw( bsd_glob ); use File::Spec::Unix; sub DefaultHandlerArgs { ( - comp_root => [ - [ local => $RT::MasonLocalComponentRoot ], - (map {[ "plugin-".$_->Name => $_->ComponentRoot ]} @{RT->Plugins}), - [ standard => $RT::MasonComponentRoot ] + comp_root => [ + RT::Interface::Web->ComponentRoots( Names => 1 ), ], default_escape_flags => 'h', data_dir => "$RT::MasonDataDir", @@ -80,27 +79,6 @@ sub DefaultHandlerArgs { ( named_component_subs => $INC{'Devel/Cover.pm'} ? 1 : 0, ) }; -# {{{ sub new - -=head2 new - - Constructs a web handler of the appropriate class. - Takes options to pass to the constructor. - -=cut - -sub new { - my $class = shift; - $class->InitSessionDir; - - if ( ($mod_perl::VERSION && $mod_perl::VERSION >= 1.9908) || $CGI::MOD_PERL) { - goto &NewApacheHandler; - } - else { - goto &NewCGIHandler; - } -} - sub InitSessionDir { # Activate the following if running httpd as root (the normal case). # Resets ownership of all files created by Mason at startup. @@ -125,81 +103,14 @@ sub InitSessionDir { } -# }}} - -# {{{ sub NewApacheHandler - -=head2 NewApacheHandler - - Takes extra options to pass to HTML::Mason::ApacheHandler->new - Returns a new Mason::ApacheHandler object - -=cut - -sub NewApacheHandler { - require HTML::Mason::ApacheHandler; - return NewHandler('HTML::Mason::ApacheHandler', args_method => "CGI", @_); -} - -# }}} - -# {{{ sub NewCGIHandler - -=head2 NewCGIHandler - - Returns a new Mason::CGIHandler object - -=cut - -sub NewCGIHandler { - require HTML::Mason::CGIHandler; - return NewHandler( - 'HTML::Mason::CGIHandler', - out_method => sub { - my $m = HTML::Mason::Request->instance; - my $r = $m->cgi_request; - - # Send headers if they have not been sent by us or by user. - $r->send_http_header unless $r->http_header_sent; - - # Set up a default - $r->content_type('text/html; charset=utf-8') - unless $r->content_type; - - if ( $r->content_type =~ /charset=([\w-]+)$/ ) { - my $enc = $1; - if ( lc $enc !~ /utf-?8$/ ) { - for my $str (@_) { - next unless $str; - - # only encode perl internal strings - next unless utf8::is_utf8($str); - $str = Encode::encode( $enc, $str ); - } - } - } - - # default to utf8 encoding - for my $str (@_) { - next unless $str; - next unless utf8::is_utf8($str); - $str = Encode::encode( 'utf8', $str ); - } - - # We could perhaps install a new, faster out_method here that - # wouldn't have to keep checking whether headers have been - # sent and what the $r->method is. That would require - # additions to the Request interface, though. - print STDOUT grep {defined} @_; - }, - @_ - ); -} +use UNIVERSAL::require; sub NewHandler { my $class = shift; + $class->require or die $!; my $handler = $class->new( DefaultHandlerArgs(), + RT->Config->Get('MasonParameters'), @_ ); @@ -208,6 +119,23 @@ sub NewHandler { return($handler); } +=head2 _mason_dir_index + +=cut + +sub _mason_dir_index { + my ($self, $interp, $path) = @_; + $path =~ s!/$!!; + if ( !$interp->comp_exists( $path ) + && $interp->comp_exists( $path . "/index.html" ) ) + { + return $path . "/index.html"; + } + + return $path; +} + + =head2 CleanupRequest Clean ups globals, caches and other things that could be still @@ -266,9 +194,97 @@ sub CleanupRequest { delete $RT::System->{attributes}; # Explicitly remove any tmpfiles that GPG opened, and close their - # filehandles. - File::Temp::cleanup; + # filehandles. unless we are doing inline psgi testing, which kills all the tmp file created by tests. + File::Temp::cleanup() + unless $INC{'Test/WWW/Mechanize/PSGI.pm'}; + + +} + + +# PSGI App + +use RT::Interface::Web::Handler; +use CGI::Emulate::PSGI; +use Plack::Request; +use Plack::Response; +use Plack::Util; +use Encode qw(encode_utf8); + +sub PSGIApp { + my $self = shift; + + # XXX: this is fucked + require HTML::Mason::CGIHandler; + require HTML::Mason::PSGIHandler::Streamy; + my $h = RT::Interface::Web::Handler::NewHandler('HTML::Mason::PSGIHandler::Streamy'); + + $self->InitSessionDir; + + return sub { + my $env = shift; + RT::ConnectToDatabase() unless RT->InstallMode; + + my $req = Plack::Request->new($env); + + # CGI.pm normalizes .. out of paths so when you requested + # /NoAuth/../Ticket/Display.html we saw Ticket/Display.html + # PSGI doesn't normalize .. so we have to deal ourselves. + if ( $req->path_info =~ m{/\.} ) { + $RT::Logger->crit("Invalid request for ".$req->path_info." aborting"); + my $res = Plack::Response->new(400); + return $self->_psgi_response_cb($res->finalize,sub { $self->CleanupRequest }); + } + $env->{PATH_INFO} = $self->_mason_dir_index( $h->interp, $req->path_info); + + my $ret; + { + # XXX: until we get rid of all $ENV stuff. + local %ENV = (%ENV, CGI::Emulate::PSGI->emulate_environment($env)); + + $ret = $h->handle_psgi($env); + } + + $RT::Logger->crit($@) if $@ && $RT::Logger; + warn $@ if $@ && !$RT::Logger; + if (ref($ret) eq 'CODE') { + my $orig_ret = $ret; + $ret = sub { + my $respond = shift; + local %ENV = (%ENV, CGI::Emulate::PSGI->emulate_environment($env)); + $orig_ret->($respond); + }; + } + + return $self->_psgi_response_cb($ret, + sub { + $self->CleanupRequest() + }); +}; + +sub _psgi_response_cb { + my $self = shift; + my ($ret, $cleanup) = @_; + Plack::Util::response_cb + ($ret, + sub { + my $res = shift; + + if ( RT->Config->Get('Framebusting') ) { + # XXX TODO: Do we want to make the value of this header configurable? + Plack::Util::header_set($res->[1], 'X-Frame-Options' => 'DENY'); + } + + return sub { + if (!defined $_[0]) { + $cleanup->(); + return ''; + } + return utf8::is_utf8($_[0]) ? encode_utf8($_[0]) : $_[0]; + return $_[0]; + }; + }); + } } -# }}} 1; diff --git a/rt/lib/RT/Interface/Web/Menu.pm b/rt/lib/RT/Interface/Web/Menu.pm index 3b6ce888e..6b351e94b 100644 --- a/rt/lib/RT/Interface/Web/Menu.pm +++ b/rt/lib/RT/Interface/Web/Menu.pm @@ -2,7 +2,7 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC # <sales@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -48,22 +48,267 @@ package RT::Interface::Web::Menu; +use strict; +use warnings; + + +use base qw/Class::Accessor::Fast/; +use URI; +use Scalar::Util qw(weaken); + +__PACKAGE__->mk_accessors(qw( + key title description raw_html escape_title sort_order target class +)); + +=head1 NAME + +RT::Interface::Web::Menu - Handle the API for menu navigation + +=head1 METHODS + +=head2 new PARAMHASH + +Creates a new L<RT::Interface::Web::Menu> object. Possible keys in the +I<PARAMHASH> are L</parent>, L</title>, L</description>, L</path>, +L</raw_html>, L<escape_title>, L</sort_order>, L</class>, L</target> and +L</active>. See the subroutines with the respective name below for +each option's use. + +=cut sub new { - my $class = shift; - my $self = bless {}, $class; - $self->{'root_node'} = RT::Interface::Web::Menu::Item->new(); + my $package = shift; + my $args = ref($_[0]) eq 'HASH' ? shift @_ : {@_}; + + my $parent = delete $args->{'parent'}; + $args->{sort_order} ||= 0; + + # Class::Accessor only wants a hashref; + my $self = $package->SUPER::new( $args ); + + # make sure our reference is weak + $self->parent($parent) if defined $parent; + return $self; } -sub as_hash_of_hashes { +=head2 title [STRING] + +Sets or returns the string that the menu item will be displayed as. + +=head2 escape_title [BOOLEAN] + +Sets or returns whether or not to HTML escape the title before output. + +=head2 parent [MENU] +Gets or sets the parent L<RT::Interface::Web::Menu> of this item; this defaults +to null. This ensures that the reference is weakened. + +=head2 raw_html [STRING] + +Sets the content of this menu item to a raw blob of HTML. When building the +menu, rather than constructing a link, we will return this raw content. No +escaping is done. + +=cut + +sub parent { + my $self = shift; + if (@_) { + $self->{parent} = shift; + weaken $self->{parent}; + } + + return $self->{parent}; +} + + +=head2 sort_order [NUMBER] + +Gets or sets the sort order of the item, as it will be displayed under +the parent. This defaults to adding onto the end. + +=head2 target [STRING] + +Get or set the frame or pseudo-target for this link. something like L<_blank> + +=head2 class [STRING] + +Gets or sets the CSS class the menu item should have in addition to the default +classes. This is only used if L</raw_html> isn't specified. + +=head2 path + +Gets or sets the URL that the menu's link goes to. If the link +provided is not absolute (does not start with a "/"), then is is +treated as relative to it's parent's path, and made absolute. + +=cut + +sub path { + my $self = shift; + if (@_) { + $self->{path} = shift; + $self->{path} = URI->new_abs($self->{path}, $self->parent->path . "/")->as_string + if defined $self->{path} and $self->parent and $self->parent->path; + $self->{path} =~ s!///!/! if $self->{path}; + } + return $self->{path}; +} + +=head2 active [BOOLEAN] + +Gets or sets if the menu item is marked as active. Setting this +cascades to all of the parents of the menu item. + +This is currently B<unused>. + +=cut + +sub active { + my $self = shift; + if (@_) { + $self->{active} = shift; + $self->parent->active($self->{active}) if defined $self->parent; + } + return $self->{active}; } -sub root { +=head2 child KEY [, PARAMHASH] + +If only a I<KEY> is provided, returns the child with that I<KEY>. + +Otherwise, creates or overwrites the child with that key, passing the +I<PARAMHASH> to L<RT::Interface::Web::Menu/new>. Additionally, the paramhash's +L</title> defaults to the I<KEY>, and the L</sort_order> defaults to the +pre-existing child's sort order (if a C<KEY> is being over-written) or +the end of the list, if it is a new C<KEY>. + +If the paramhash contains a key called C<menu>, that will be used instead +of creating a new RT::Interface::Web::Menu. + + +=cut + +sub child { + my $self = shift; + my $key = shift; + my $proto = ref $self || $self; + + if ( my %args = @_ ) { + + # Clear children ordering cache + delete $self->{children_list}; + + my $child; + if ( $child = $args{menu} ) { + $child->parent($self); + } else { + $child = $proto->new( + { parent => $self, + key => $key, + title => $key, + escape_title=> 1, + %args + } + ); + } + $self->{children}{$key} = $child; + + $child->sort_order( $args{sort_order} || (scalar values %{ $self->{children} }) ) + unless ($child->sort_order()); + + # URL is relative to parents, and cached, so set it up now + $child->path( $child->{path} ); + + # Figure out the URL + my $path = $child->path; + + # Activate it + if ( defined $path and length $path ) { + my $base_path = $HTML::Mason::Commands::r->path_info; + my $query = $HTML::Mason::Commands::m->cgi_object->query_string; + $base_path .= "?$query" if defined $query and length $query; + + $base_path =~ s/index\.html$//; + $base_path =~ s/\/+$//; + $path =~ s/index\.html$//; + $path =~ s/\/+$//; + + if ( $path eq $base_path ) { + $self->{children}{$key}->active(1); + } + } + } + + return $self->{children}{$key}; +} + +=head2 active_child + +Returns the first active child node, or C<undef> is there is none. + +=cut + +sub active_child { + my $self = shift; + foreach my $kid ($self->children) { + return $kid if $kid->active; + } + return undef; +} + + +=head2 delete KEY + +Removes the child with the provided I<KEY>. + +=cut + +sub delete { + my $self = shift; + my $key = shift; + delete $self->{children_list}; + delete $self->{children}{$key}; +} + + +=head2 has_children + +Returns true if there are any children on this menu + +=cut + +sub has_children { + my $self = shift; + if (@{ $self->children}) { + return 1 + } else { + return 0; + } +} + + +=head2 children + +Returns the children of this menu item in sorted order; as an array in +array context, or as an array reference in scalar context. + +=cut + +sub children { my $self = shift; - return $self->{'root_node'}; + my @kids; + if ($self->{children_list}) { + @kids = @{$self->{children_list}}; + } else { + @kids = values %{$self->{children} || {}}; + @kids = sort {$a->{sort_order} <=> $b->{sort_order}} @kids; + $self->{children_list} = \@kids; + } + return wantarray ? @kids : \@kids; } 1; diff --git a/rt/lib/RT/Interface/Web/Menu/Item.pm b/rt/lib/RT/Interface/Web/Menu/Item.pm deleted file mode 100644 index 29fb13bcc..000000000 --- a/rt/lib/RT/Interface/Web/Menu/Item.pm +++ /dev/null @@ -1,88 +0,0 @@ -# BEGIN BPS TAGGED BLOCK {{{ -# -# COPYRIGHT: -# -# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC -# <sales@bestpractical.com> -# -# (Except where explicitly superseded by other copyright notices) -# -# -# LICENSE: -# -# This work is made available to you under the terms of Version 2 of -# the GNU General Public License. A copy of that license should have -# been provided with this software, but in any event can be snarfed -# from www.gnu.org. -# -# This work is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -# 02110-1301 or visit their web page on the internet at -# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. -# -# -# CONTRIBUTION SUBMISSION POLICY: -# -# (The following paragraph is not intended to limit the rights granted -# to you to modify and distribute this software under the terms of -# the GNU General Public License and is only of importance to you if -# you choose to contribute your changes and enhancements to the -# community by submitting them to Best Practical Solutions, LLC.) -# -# By intentionally submitting any modifications, corrections or -# derivatives to this work, or any other work intended for use with -# Request Tracker, to Best Practical Solutions, LLC, you confirm that -# you are the copyright holder for those contributions and you grant -# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, -# royalty-free, perpetual, license to use, copy, create derivative -# works based on those contributions, and sublicense and distribute -# those contributions and any derivatives thereof. -# -# END BPS TAGGED BLOCK }}} - -package RT::Interface::Web::Menu::Item; -use strict; -use warnings; - -sub new { - my $class = shift; - my $self = bless {},$class; - $self->{'_attributes'} = {}; - return($self); -} - -sub label { my $self = shift; $self->_accessor( label => @_) } ; -sub absolute_url { my $self = shift; $self->_accessor( absolute_url => @_) } ; -sub rt_path { my $self = shift; $self->_accessor( rt_path => @_) } ; -sub hilight { my $self = shift; $self->_accessor( hilight => @_); - $self->parent->hilight(1); - } ; -sub sort_order { my $self = shift; $self->_accessor( sort_order => @_) } ; - -sub add_child { -} - -sub delete { -} - -sub children { - -} - -sub _accessor { - my $self = shift; - my $key = shift; - if (@_){ - $self->{'attributes'}->{$key} = shift; - - } - return $self->{'_attributes'}->{$key}; -} - -1; diff --git a/rt/lib/RT/Interface/Web/QueryBuilder.pm b/rt/lib/RT/Interface/Web/QueryBuilder.pm index 09b95398c..79a0b9718 100755 --- a/rt/lib/RT/Interface/Web/QueryBuilder.pm +++ b/rt/lib/RT/Interface/Web/QueryBuilder.pm @@ -2,7 +2,7 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC # <sales@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) diff --git a/rt/lib/RT/Interface/Web/QueryBuilder/Tree.pm b/rt/lib/RT/Interface/Web/QueryBuilder/Tree.pm index 493ab444d..e2ec1e58d 100755 --- a/rt/lib/RT/Interface/Web/QueryBuilder/Tree.pm +++ b/rt/lib/RT/Interface/Web/QueryBuilder/Tree.pm @@ -2,7 +2,7 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC # <sales@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -260,20 +260,24 @@ sub ParseSQL { my $class; if ( exists $lcfield{ lc $main_key } ) { - $class = $field{ $main_key }->[0]; $key =~ s/^[^.]+/ $lcfield{ lc $main_key } /e; + ($main_key) = split /[.]/, $key; # make the case right + $class = $field{ $main_key }->[0]; } unless( $class ) { push @results, [ $args{'CurrentUser'}->loc("Unknown field: [_1]", $key), -1 ] } - $value =~ s/'/\\'/g; if ( lc $op eq 'is' || lc $op eq 'is not' ) { $value = 'NULL'; # just fix possible mistakes here } elsif ( $value !~ /^[+-]?[0-9]+$/ ) { + $value =~ s/(['\\])/\\$1/g; $value = "'$value'"; } - $key = "'$key'" if $key =~ /^CF./; + + if ($key =~ s/(['\\])/\\$1/g or $key =~ /\s/) { + $key = "'$key'"; + } my $clause = { Key => $key, Op => $op, Value => $value }; $node->addChild( __PACKAGE__->new( $clause ) ); diff --git a/rt/lib/RT/Interface/Web/Request.pm b/rt/lib/RT/Interface/Web/Request.pm index 84dd28dd6..d0865117d 100644 --- a/rt/lib/RT/Interface/Web/Request.pm +++ b/rt/lib/RT/Interface/Web/Request.pm @@ -2,7 +2,7 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC # <sales@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) @@ -52,51 +52,16 @@ use strict; use warnings; our $VERSION = '0.30'; -use base qw(HTML::Mason::Request); +use HTML::Mason::PSGIHandler; +use base qw(HTML::Mason::Request::PSGI); +use Params::Validate qw(:all); sub new { my $class = shift; - - my $new_class = $HTML::Mason::ApacheHandler::VERSION ? - 'HTML::Mason::Request::ApacheHandler' : - $HTML::Mason::CGIHandler::VERSION ? - 'HTML::Mason::Request::CGI' : - 'HTML::Mason::Request'; - - $class->alter_superclass( $new_class ); - $class->valid_params( %{ $new_class->valid_params } ); + $class->valid_params( %{ $class->valid_params },cgi_request => { type => OBJECT, optional => 1 } ); return $class->SUPER::new(@_); } -# XXX TODO: This alter_superclass replaces teh funcitonality in Mason 1.39 -# with code which doesn't trigger a bug in Perl 5.10. -# (Perl 5.10.0 does NOT take kindly to having its @INC entries changed) -# http://rt.perl.org/rt3/Public/Bug/Display.html?id=54566 -# -# This routine can be removed when either: -# * RT depends on a version of mason which contains this fix -# * Perl 5.10.0 is not supported for running RT -sub alter_superclass { - my $class = shift; - my $new_super = shift; - my $isa_ref; - { no strict 'refs'; my @entries = @{$class."::ISA"}; $isa_ref = \@entries; } - - # handles multiple inheritance properly and preserve - # inheritance order - for ( my $x = 0; $x <= $#{$isa_ref} ; $x++ ) { - if ( $isa_ref->[$x]->isa('HTML::Mason::Request') ) { - my $old_super = $isa_ref->[$x]; - $isa_ref->[$x] = $new_super - if ( $old_super ne $new_super ); - last; - } - } - - { no strict 'refs'; @{$class."::ISA"} = @$isa_ref; } - $class->valid_params( %{ $class->valid_params } ); -} - =head2 callback @@ -153,11 +118,7 @@ sub callback { unless ( $callbacks ) { $callbacks = []; my $path = "/Callbacks/*$page/$name"; - my @roots = map $_->[1], - $HTML::Mason::VERSION <= 1.28 - ? $self->interp->resolver->comp_root_array - : $self->interp->comp_root_array; - + my @roots = RT::Interface::Web->ComponentRoots; my %seen; @$callbacks = ( grep defined && length, diff --git a/rt/lib/RT/Interface/Web/Session.pm b/rt/lib/RT/Interface/Web/Session.pm index 8ce8afd2b..c5b88f127 100644 --- a/rt/lib/RT/Interface/Web/Session.pm +++ b/rt/lib/RT/Interface/Web/Session.pm @@ -2,7 +2,7 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC # <sales@bestpractical.com> # # (Except where explicitly superseded by other copyright notices) diff --git a/rt/lib/RT/Interface/Web/Standalone.pm b/rt/lib/RT/Interface/Web/Standalone.pm deleted file mode 100755 index 3157e315e..000000000 --- a/rt/lib/RT/Interface/Web/Standalone.pm +++ /dev/null @@ -1,126 +0,0 @@ -# BEGIN BPS TAGGED BLOCK {{{ -# -# COPYRIGHT: -# -# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC -# <sales@bestpractical.com> -# -# (Except where explicitly superseded by other copyright notices) -# -# -# LICENSE: -# -# This work is made available to you under the terms of Version 2 of -# the GNU General Public License. A copy of that license should have -# been provided with this software, but in any event can be snarfed -# from www.gnu.org. -# -# This work is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -# 02110-1301 or visit their web page on the internet at -# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. -# -# -# CONTRIBUTION SUBMISSION POLICY: -# -# (The following paragraph is not intended to limit the rights granted -# to you to modify and distribute this software under the terms of -# the GNU General Public License and is only of importance to you if -# you choose to contribute your changes and enhancements to the -# community by submitting them to Best Practical Solutions, LLC.) -# -# By intentionally submitting any modifications, corrections or -# derivatives to this work, or any other work intended for use with -# Request Tracker, to Best Practical Solutions, LLC, you confirm that -# you are the copyright holder for those contributions and you grant -# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, -# royalty-free, perpetual, license to use, copy, create derivative -# works based on those contributions, and sublicense and distribute -# those contributions and any derivatives thereof. -# -# END BPS TAGGED BLOCK }}} - -use strict; -use warnings; -package RT::Interface::Web::Standalone; - -use base 'HTTP::Server::Simple::Mason'; -use RT::Interface::Web::Handler; -use RT::Interface::Web; -use URI; - -sub handler_class { "RT::Interface::Web::Handler" } - -sub setup_escapes { - my $self = shift; - my $handler = shift; - - # Override HTTP::Server::Simple::Mason's version of this method to do - # nothing. (RT::Interface::Web::Handler does this already for us in - # NewHandler.) -} - -sub default_mason_config { - return RT->Config->Get('MasonParameters'); -} - -sub handle_request { - - my $self = shift; - my $cgi = shift; - - Module::Refresh->refresh if RT->Config->Get('DevelMode'); - RT::ConnectToDatabase() unless RT->InstallMode; - - # Each environment has its own way of handling .. and so on in paths, - # so RT consistently forbids such paths. - if ( $cgi->path_info =~ m{/\.} ) { - $RT::Logger->crit("Invalid request for ".$cgi->path_info." aborting"); - print STDOUT "HTTP/1.0 400\r\n\r\n"; - return RT::Interface::Web::Handler->CleanupRequest(); - } - - $self->SUPER::handle_request($cgi); - $RT::Logger->crit($@) if $@ && $RT::Logger; - warn $@ if $@ && !$RT::Logger; - RT::Interface::Web::Handler->CleanupRequest(); -} - -sub net_server { - my $self = shift; - $self->{rt_net_server} = shift if @_; - return $self->{rt_net_server}; -} - - -=head2 print_banner - -This routine prints a banner before the server request-handling loop -starts. - -Methods below this point are probably not terribly useful to define -yourself in subclasses. - -=cut - -sub print_banner { - my $self = shift; - - my $url = URI->new( RT->Config->Get('WebBaseURL')); - $url->host('127.0.0.1') if ($url->host() eq 'localhost'); - $url->port($self->port); - print( - "You can connect to your server at " - . $url->canonical - . "\n" ); - -} - - -1; diff --git a/rt/lib/RT/Interface/Web/Standalone/PreFork.pm b/rt/lib/RT/Interface/Web/Standalone/PreFork.pm deleted file mode 100644 index f569e4f00..000000000 --- a/rt/lib/RT/Interface/Web/Standalone/PreFork.pm +++ /dev/null @@ -1,103 +0,0 @@ -# BEGIN BPS TAGGED BLOCK {{{ -# -# COPYRIGHT: -# -# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC -# <sales@bestpractical.com> -# -# (Except where explicitly superseded by other copyright notices) -# -# -# LICENSE: -# -# This work is made available to you under the terms of Version 2 of -# the GNU General Public License. A copy of that license should have -# been provided with this software, but in any event can be snarfed -# from www.gnu.org. -# -# This work is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA -# 02110-1301 or visit their web page on the internet at -# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. -# -# -# CONTRIBUTION SUBMISSION POLICY: -# -# (The following paragraph is not intended to limit the rights granted -# to you to modify and distribute this software under the terms of -# the GNU General Public License and is only of importance to you if -# you choose to contribute your changes and enhancements to the -# community by submitting them to Best Practical Solutions, LLC.) -# -# By intentionally submitting any modifications, corrections or -# derivatives to this work, or any other work intended for use with -# Request Tracker, to Best Practical Solutions, LLC, you confirm that -# you are the copyright holder for those contributions and you grant -# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, -# royalty-free, perpetual, license to use, copy, create derivative -# works based on those contributions, and sublicense and distribute -# those contributions and any derivatives thereof. -# -# END BPS TAGGED BLOCK }}} - -use warnings; -use strict; - -package RT::Interface::Web::Standalone::PreFork; -use base qw/Net::Server::PreFork/; - -my %option_map = ( - min_servers => 'StandaloneMinServers', - max_servers => 'StandaloneMaxServers', - min_spare_servers => 'StandaloneMinSpareServers', - max_spare_servers => 'StandaloneMaxSpareServers', - max_requests => 'StandaloneMaxRequests', -); - -=head2 default_values - -Produces the default values for L<Net::Server> configuration from RT's config -files. - -=cut - -sub default_values { - my %forking = ( - map { $_ => RT->Config->Get( $option_map{$_} ) } - grep { defined( RT->Config->Get( $option_map{$_} ) ) } - keys %option_map, - ); - - return { - %forking, - log_level => 1, - RT->Config->Get('NetServerOptions') - }; -} - -=head2 post_bind_hook - -After binding to the specified ports, let the user know that the server is -prepared to handle connections. - -=cut - -sub post_bind_hook { - my $self = shift; - my @ports = @{ $self->{server}->{port} }; - - print $0 - . ": You can connect to your server at " - . (join ' , ', map { "http://localhost:$_/" } @ports) - . "\n"; - - $self->SUPER::post_bind_hook(@_); -} - -1; |