From: Mark Wells Date: Fri, 29 Jan 2016 06:02:56 +0000 (-0800) Subject: remove .orig junk under rt X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=commitdiff_plain;h=c21a62102dc7ad064331872637e046c342d50c2e remove .orig junk under rt --- diff --git a/rt/lib/RT/Action/CreateTickets.pm.orig b/rt/lib/RT/Action/CreateTickets.pm.orig deleted file mode 100644 index 542cbd27b..000000000 --- a/rt/lib/RT/Action/CreateTickets.pm.orig +++ /dev/null @@ -1,1288 +0,0 @@ -# BEGIN BPS TAGGED BLOCK {{{ -# -# COPYRIGHT: -# -# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC -# -# -# (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::Action::CreateTickets; -use base 'RT::Action'; - -use strict; -use warnings; - -use MIME::Entity; - -=head1 NAME - -RT::Action::CreateTickets - Create one or more tickets according to an externally supplied template - -=head1 SYNOPSIS - - ===Create-Ticket: codereview - Subject: Code review for {$Tickets{'TOP'}->Subject} - Depended-On-By: TOP - Content: Someone has created a ticket. you should review and approve it, - so they can finish their work - ENDOFCONTENT - -=head1 DESCRIPTION - -The CreateTickets ScripAction allows you to create automated workflows in RT, -creating new tickets in response to actions and conditions from other -tickets. - -=head2 Format - -CreateTickets uses the RT template configured in the scrip as a template -for an ordered set of tickets to create. The basic format is as follows: - - ===Create-Ticket: identifier - Param: Value - Param2: Value - Param3: Value - Content: Blah - blah - blah - ENDOFCONTENT - ===Create-Ticket: id2 - Param: Value - Content: Blah - ENDOFCONTENT - -As shown, you can put one or more C<===Create-Ticket:> sections in -a template. Each C<===Create-Ticket:> section is evaluated as its own -L object, which means that you can embed snippets -of Perl inside the L using C<{}> delimiters, but that -such sections absolutely can not span a C<===Create-Ticket:> boundary. - -Note that each C must come right after the C on the same -line. The C param can extend over multiple lines, but the text -of the first line must start right after C. Don't try to start -your C section with a newline. - -After each ticket is created, it's stuffed into a hash called C<%Tickets> -making it available during the creation of other tickets during the -same ScripAction. The hash key for each ticket is C, -where C<[identifier]> is the value you put after C<===Create-Ticket:>. The hash -is prepopulated with the ticket which triggered the ScripAction as -C<$Tickets{'TOP'}>. You can also access that ticket using the shorthand -C. - -A simple example: - - ===Create-Ticket: codereview - Subject: Code review for {$Tickets{'TOP'}->Subject} - Depended-On-By: TOP - Content: Someone has created a ticket. you should review and approve it, - so they can finish their work - ENDOFCONTENT - -A convoluted example: - - ===Create-Ticket: approval - { # Find out who the administrators of the group called "HR" - # of which the creator of this ticket is a member - my $name = "HR"; - - my $groups = RT::Groups->new(RT->SystemUser); - $groups->LimitToUserDefinedGroups(); - $groups->Limit(FIELD => "Name", OPERATOR => "=", VALUE => "$name"); - $groups->WithMember($TransactionObj->CreatorObj->Id); - - my $groupid = $groups->First->Id; - - my $adminccs = RT::Users->new(RT->SystemUser); - $adminccs->WhoHaveRight( - Right => "AdminGroup", - Object =>$groups->First, - IncludeSystemRights => undef, - IncludeSuperusers => 0, - IncludeSubgroupMembers => 0, - ); - - our @admins; - while (my $admin = $adminccs->Next) { - push (@admins, $admin->EmailAddress); - } - } - Queue: ___Approvals - Type: approval - AdminCc: {join ("\nAdminCc: ",@admins) } - Depended-On-By: TOP - Refers-To: TOP - Subject: Approval for ticket: {$Tickets{"TOP"}->Id} - {$Tickets{"TOP"}->Subject} - Due: {time + 86400} - Content-Type: text/plain - Content: Your approval is requested for the ticket {$Tickets{"TOP"}->Id}: {$Tickets{"TOP"}->Subject} - Blah - Blah - ENDOFCONTENT - ===Create-Ticket: two - Subject: Manager approval - Type: approval - Depended-On-By: TOP - Refers-To: {$Tickets{"create-approval"}->Id} - Queue: ___Approvals - Content-Type: text/plain - Content: Your approval is requred for this ticket, too. - ENDOFCONTENT - -As shown above, you can include a block with Perl code to set up some -values for the new tickets. If you want to access a variable in the -template section after the block, you must scope it with C rather -than C. Just as with other RT templates, you can also include -Perl code in the template sections using C<{}>. - -=head2 Acceptable Fields - -A complete list of acceptable fields: - - * Queue => Name or id# of a queue - Subject => A text string - ! Status => A valid status. Defaults to 'new' - Due => Dates can be specified in seconds since the epoch - to be handled literally or in a semi-free textual - format which RT will attempt to parse. - Starts => - Started => - Resolved => - Owner => Username or id of an RT user who can and should own - this ticket; forces the owner if necessary - + Requestor => Email address - + Cc => Email address - + AdminCc => Email address - + RequestorGroup => Group name - + CcGroup => Group name - + AdminCcGroup => Group name - TimeWorked => - TimeEstimated => - TimeLeft => - InitialPriority => - FinalPriority => - Type => - +! DependsOn => - +! DependedOnBy => - +! RefersTo => - +! ReferredToBy => - +! Members => - +! MemberOf => - Content => Content. Can extend to multiple lines. Everything - within a template after a Content: header is treated - as content until we hit a line containing only - ENDOFCONTENT - ContentType => the content-type of the Content field. Defaults to - 'text/plain' - UpdateType => 'correspond' or 'comment'; used in conjunction with - 'content' if this is an update. Defaults to - 'correspond' - - CustomField- => custom field value - CF-name => custom field value - CustomField-name => custom field value - -Fields marked with an C<*> are required. - -Fields marked with a C<+> may have multiple values, simply -by repeating the fieldname on a new line with an additional value. - -Fields marked with a C have processing postponed until after all -tickets in the same actions are created. Except for C, those -fields can also take a ticket name within the same action (i.e. -the identifiers after C<===Create-Ticket:>), instead of raw ticket ID -numbers. - -When parsed, field names are converted to lowercase and have hyphens stripped. -C, C, C, C and C will -all be treated as the same thing. - -=head1 METHODS - -=cut - -my %LINKTYPEMAP = ( - MemberOf => { - Type => 'MemberOf', - Mode => 'Target', - }, - Parents => { - Type => 'MemberOf', - Mode => 'Target', - }, - Members => { - Type => 'MemberOf', - Mode => 'Base', - }, - Children => { - Type => 'MemberOf', - Mode => 'Base', - }, - HasMember => { - Type => 'MemberOf', - Mode => 'Base', - }, - RefersTo => { - Type => 'RefersTo', - Mode => 'Target', - }, - ReferredToBy => { - Type => 'RefersTo', - Mode => 'Base', - }, - DependsOn => { - Type => 'DependsOn', - Mode => 'Target', - }, - DependedOnBy => { - Type => 'DependsOn', - Mode => 'Base', - }, - -); - - -#Do what we need to do and send it out. -sub Commit { - my $self = shift; - - # Create all the tickets we care about - return (1) unless $self->TicketObj->Type eq 'ticket'; - - $self->CreateByTemplate( $self->TicketObj ); - $self->UpdateByTemplate( $self->TicketObj ); - return (1); -} - - - -sub Prepare { - my $self = shift; - - unless ( $self->TemplateObj ) { - $RT::Logger->warning("No template object handed to $self"); - } - - unless ( $self->TransactionObj ) { - $RT::Logger->warning("No transaction object handed to $self"); - - } - - unless ( $self->TicketObj ) { - $RT::Logger->warning("No ticket object handed to $self"); - - } - - my $active = 0; - if ( $self->TemplateObj->Type eq 'Perl' ) { - $active = 1; - } else { - RT->Logger->info(sprintf( - "Template #%d is type %s. You most likely want to use a Perl template instead.", - $self->TemplateObj->id, $self->TemplateObj->Type - )); - } - - $self->Parse( - Content => $self->TemplateObj->Content, - _ActiveContent => $active, - ); - return 1; - -} - - - -sub CreateByTemplate { - my $self = shift; - my $top = shift; - - $RT::Logger->debug("In CreateByTemplate"); - - my @results; - - # XXX: cargo cult programming that works. i'll be back. - - local %T::Tickets = %T::Tickets; - local $T::TOP = $T::TOP; - local $T::ID = $T::ID; - $T::Tickets{'TOP'} = $T::TOP = $top if $top; - local $T::TransactionObj = $self->TransactionObj; - - my $ticketargs; - my ( @links, @postponed ); - foreach my $template_id ( @{ $self->{'create_tickets'} } ) { - $RT::Logger->debug("Workflow: processing $template_id of $T::TOP") - if $T::TOP; - - $T::ID = $template_id; - @T::AllID = @{ $self->{'create_tickets'} }; - - ( $T::Tickets{$template_id}, $ticketargs ) - = $self->ParseLines( $template_id, \@links, \@postponed ); - - # Now we have a %args to work with. - # Make sure we have at least the minimum set of - # reasonable data and do our thang - - my ( $id, $transid, $msg ) - = $T::Tickets{$template_id}->Create(%$ticketargs); - - foreach my $res ( split( '\n', $msg ) ) { - push @results, - $T::Tickets{$template_id} - ->loc( "Ticket [_1]", $T::Tickets{$template_id}->Id ) . ': ' - . $res; - } - if ( !$id ) { - if ( $self->TicketObj ) { - $msg = "Couldn't create related ticket $template_id for " - . $self->TicketObj->Id . " " - . $msg; - } else { - $msg = "Couldn't create ticket $template_id " . $msg; - } - - $RT::Logger->error($msg); - next; - } - - $RT::Logger->debug("Assigned $template_id with $id"); - $T::Tickets{$template_id}->SetOriginObj( $self->TicketObj ) - if $self->TicketObj - && $T::Tickets{$template_id}->can('SetOriginObj'); - - } - - $self->PostProcess( \@links, \@postponed ); - - return @results; -} - -sub UpdateByTemplate { - my $self = shift; - my $top = shift; - - # XXX: cargo cult programming that works. i'll be back. - - my @results; - local %T::Tickets = %T::Tickets; - local $T::ID = $T::ID; - - my $ticketargs; - my ( @links, @postponed ); - foreach my $template_id ( @{ $self->{'update_tickets'} } ) { - $RT::Logger->debug("Update Workflow: processing $template_id"); - - $T::ID = $template_id; - @T::AllID = @{ $self->{'update_tickets'} }; - - ( $T::Tickets{$template_id}, $ticketargs ) - = $self->ParseLines( $template_id, \@links, \@postponed ); - - # Now we have a %args to work with. - # Make sure we have at least the minimum set of - # reasonable data and do our thang - - my @attribs = qw( - Subject - FinalPriority - Priority - TimeEstimated - TimeWorked - TimeLeft - Status - Queue - Due - Starts - Started - Resolved - ); - - my $id = $template_id; - $id =~ s/update-(\d+).*/$1/; - my ($loaded, $msg) = $T::Tickets{$template_id}->LoadById($id); - - unless ( $loaded ) { - $RT::Logger->error("Couldn't update ticket $template_id: " . $msg); - push @results, $self->loc( "Couldn't load ticket '[_1]'", $id ); - next; - } - - my $current = $self->GetBaseTemplate( $T::Tickets{$template_id} ); - - $template_id =~ m/^update-(.*)/; - my $base_id = "base-$1"; - my $base = $self->{'templates'}->{$base_id}; - if ($base) { - $base =~ s/\r//g; - $base =~ s/\n+$//; - $current =~ s/\n+$//; - - # If we have no base template, set what we can. - if ( $base ne $current ) { - push @results, - "Could not update ticket " - . $T::Tickets{$template_id}->Id - . ": Ticket has changed"; - next; - } - } - push @results, $T::Tickets{$template_id}->Update( - AttributesRef => \@attribs, - ARGSRef => $ticketargs - ); - - if ( $ticketargs->{'Owner'} ) { - ($id, $msg) = $T::Tickets{$template_id}->SetOwner($ticketargs->{'Owner'}, "Force"); - push @results, $msg unless $msg eq $self->loc("That user already owns that ticket"); - } - - push @results, - $self->UpdateWatchers( $T::Tickets{$template_id}, $ticketargs ); - - push @results, - $self->UpdateCustomFields( $T::Tickets{$template_id}, $ticketargs ); - - next unless $ticketargs->{'MIMEObj'}; - if ( $ticketargs->{'UpdateType'} =~ /^(private|comment)$/i ) { - my ( $Transaction, $Description, $Object ) - = $T::Tickets{$template_id}->Comment( - BccMessageTo => $ticketargs->{'Bcc'}, - MIMEObj => $ticketargs->{'MIMEObj'}, - TimeTaken => $ticketargs->{'TimeWorked'} - ); - push( @results, - $T::Tickets{$template_id} - ->loc( "Ticket [_1]", $T::Tickets{$template_id}->id ) - . ': ' - . $Description ); - } elsif ( $ticketargs->{'UpdateType'} =~ /^(public|response|correspond)$/i ) { - my ( $Transaction, $Description, $Object ) - = $T::Tickets{$template_id}->Correspond( - BccMessageTo => $ticketargs->{'Bcc'}, - MIMEObj => $ticketargs->{'MIMEObj'}, - TimeTaken => $ticketargs->{'TimeWorked'} - ); - push( @results, - $T::Tickets{$template_id} - ->loc( "Ticket [_1]", $T::Tickets{$template_id}->id ) - . ': ' - . $Description ); - } else { - push( - @results, - $T::Tickets{$template_id}->loc( - "Update type was neither correspondence nor comment.") - . " " - . $T::Tickets{$template_id}->loc("Update not recorded.") - ); - } - } - - $self->PostProcess( \@links, \@postponed ); - - return @results; -} - -=head2 Parse - -Takes (in order) template content, a default queue, a default requestor, and -active (a boolean flag). - -Parses a template in the template content, defaulting queue and requestor if -unspecified in the template to the values provided as arguments. - -If the active flag is true, then we'll use L to parse the -templates, allowing you to embed active Perl in your templates. - -=cut - -sub Parse { - my $self = shift; - my %args = ( - Content => undef, - Queue => undef, - Requestor => undef, - _ActiveContent => undef, - @_ - ); - - if ( $args{'_ActiveContent'} ) { - $self->{'UsePerlTextTemplate'} = 1; - } else { - - $self->{'UsePerlTextTemplate'} = 0; - } - - if ( substr( $args{'Content'}, 0, 3 ) eq '===' ) { - $self->_ParseMultilineTemplate(%args); - } elsif ( $args{'Content'} =~ /(?:\t|,)/i ) { - $self->_ParseXSVTemplate(%args); - } else { - RT->Logger->error("Invalid Template Content (Couldn't find ===, and is not a csv/tsv template) - unable to parse: $args{Content}"); - } -} - -=head2 _ParseMultilineTemplate - -Parses mulitline templates. Things like: - - ===Create-Ticket: ... - -Takes the same arguments as L. - -=cut - -sub _ParseMultilineTemplate { - my $self = shift; - my %args = (@_); - - my $template_id; - my ( $queue, $requestor ); - $RT::Logger->debug("Line: ==="); - foreach my $line ( split( /\n/, $args{'Content'} ) ) { - $line =~ s/\r$//; - $RT::Logger->debug( "Line: $line" ); - if ( $line =~ /^===/ ) { - if ( $template_id && !$queue && $args{'Queue'} ) { - $self->{'templates'}->{$template_id} - .= "Queue: $args{'Queue'}\n"; - } - if ( $template_id && !$requestor && $args{'Requestor'} ) { - $self->{'templates'}->{$template_id} - .= "Requestor: $args{'Requestor'}\n"; - } - $queue = 0; - $requestor = 0; - } - if ( $line =~ /^===Create-Ticket: (.*)$/ ) { - $template_id = "create-$1"; - $RT::Logger->debug("**** Create ticket: $template_id"); - push @{ $self->{'create_tickets'} }, $template_id; - } elsif ( $line =~ /^===Update-Ticket: (.*)$/ ) { - $template_id = "update-$1"; - $RT::Logger->debug("**** Update ticket: $template_id"); - push @{ $self->{'update_tickets'} }, $template_id; - } elsif ( $line =~ /^===Base-Ticket: (.*)$/ ) { - $template_id = "base-$1"; - $RT::Logger->debug("**** Base ticket: $template_id"); - push @{ $self->{'base_tickets'} }, $template_id; - } elsif ( $line =~ /^===#.*$/ ) { # a comment - next; - } else { - if ( $line =~ /^Queue:(.*)/i ) { - $queue = 1; - my $value = $1; - $value =~ s/^\s//; - $value =~ s/\s$//; - if ( !$value && $args{'Queue'} ) { - $value = $args{'Queue'}; - $line = "Queue: $value"; - } - } - if ( $line =~ /^Requestors?:(.*)/i ) { - $requestor = 1; - my $value = $1; - $value =~ s/^\s//; - $value =~ s/\s$//; - if ( !$value && $args{'Requestor'} ) { - $value = $args{'Requestor'}; - $line = "Requestor: $value"; - } - } - $self->{'templates'}->{$template_id} .= $line . "\n"; - } - } - if ( $template_id && !$queue && $args{'Queue'} ) { - $self->{'templates'}->{$template_id} .= "Queue: $args{'Queue'}\n"; - } - } - -sub ParseLines { - my $self = shift; - my $template_id = shift; - my $links = shift; - my $postponed = shift; - - my $content = $self->{'templates'}->{$template_id}; - - if ( $self->{'UsePerlTextTemplate'} ) { - - $RT::Logger->debug( - "Workflow: evaluating\n$self->{templates}{$template_id}"); - - my $template = Text::Template->new( - TYPE => 'STRING', - SOURCE => $content - ); - - my $err; - $content = $template->fill_in( - PACKAGE => 'T', - BROKEN => sub { - $err = {@_}->{error}; - } - ); - - $RT::Logger->debug("Workflow: yielding $content"); - - if ($err) { - $RT::Logger->error( "Ticket creation failed: " . $err ); - while ( my ( $k, $v ) = each %T::X ) { - $RT::Logger->debug( - "Eliminating $template_id from ${k}'s parents."); - delete $v->{$template_id}; - } - next; - } - } - - my $TicketObj ||= RT::Ticket->new( $self->CurrentUser ); - - my %args; - my %original_tags; - my @lines = ( split( /\n/, $content ) ); - while ( defined( my $line = shift @lines ) ) { - if ( $line =~ /^(.*?):(?:\s+)(.*?)(?:\s*)$/ ) { - my $value = $2; - my $original_tag = $1; - my $tag = lc($original_tag); - $tag =~ s/-//g; - $tag =~ s/^(requestor|cc|admincc)s?$/$1/i; - - $original_tags{$tag} = $original_tag; - - if ( ref( $args{$tag} ) ) - { #If it's an array, we want to push the value - push @{ $args{$tag} }, $value; - } elsif ( defined( $args{$tag} ) ) - { #if we're about to get a second value, make it an array - $args{$tag} = [ $args{$tag}, $value ]; - } else { #if there's nothing there, just set the value - $args{$tag} = $value; - } - - if ( $tag =~ /^content$/i ) { #just build up the content - # convert it to an array - $args{$tag} = defined($value) ? [ $value . "\n" ] : []; - while ( defined( my $l = shift @lines ) ) { - last if ( $l =~ /^ENDOFCONTENT\s*$/ ); - push @{ $args{'content'} }, $l . "\n"; - } - } else { - # if it's not content, strip leading and trailing spaces - if ( $args{$tag} ) { - $args{$tag} =~ s/^\s+//g; - $args{$tag} =~ s/\s+$//g; - } - if ( - ($tag =~ /^(requestor|cc|admincc)(group)?$/i - or grep {lc $_ eq $tag} keys %LINKTYPEMAP) - and $args{$tag} =~ /,/ - ) { - $args{$tag} = [ split /,\s*/, $args{$tag} ]; - } - } - } - } - - foreach my $date (qw(due starts started resolved)) { - my $dateobj = RT::Date->new( $self->CurrentUser ); - next unless $args{$date}; - if ( $args{$date} =~ /^\d+$/ ) { - $dateobj->Set( Format => 'unix', Value => $args{$date} ); - } else { - eval { - $dateobj->Set( Format => 'iso', Value => $args{$date} ); - }; - if ($@ or $dateobj->Unix <= 0) { - $dateobj->Set( Format => 'unknown', Value => $args{$date} ); - } - } - $args{$date} = $dateobj->ISO; - } - - foreach my $role (qw(requestor cc admincc)) { - next unless my $value = $args{ $role . 'group' }; - - my $group = RT::Group->new( $self->CurrentUser ); - $group->LoadUserDefinedGroup( $value ); - unless ( $group->id ) { - $RT::Logger->error("Couldn't load group '$value'"); - next; - } - - $args{ $role } = $args{ $role } ? [$args{ $role }] : [] - unless ref $args{ $role }; - push @{ $args{ $role } }, $group->PrincipalObj->id; - } - - $args{'requestor'} ||= $self->TicketObj->Requestors->MemberEmailAddresses - if $self->TicketObj; - - $args{'type'} ||= 'ticket'; - - my %ticketargs = ( - Queue => $args{'queue'}, - Subject => $args{'subject'}, - Status => $args{'status'} || 'new', - Due => $args{'due'}, - Starts => $args{'starts'}, - Started => $args{'started'}, - Resolved => $args{'resolved'}, - Owner => $args{'owner'}, - Requestor => $args{'requestor'}, - Cc => $args{'cc'}, - AdminCc => $args{'admincc'}, - TimeWorked => $args{'timeworked'}, - TimeEstimated => $args{'timeestimated'}, - TimeLeft => $args{'timeleft'}, - InitialPriority => $args{'initialpriority'} || 0, - FinalPriority => $args{'finalpriority'} || 0, - SquelchMailTo => $args{'squelchmailto'}, - Type => $args{'type'}, - $self->Rules - ); - - if ( $args{content} ) { - my $mimeobj = MIME::Entity->build( - Type => $args{'contenttype'} || 'text/plain', - Charset => 'UTF-8', - Data => [ map {Encode::encode( "UTF-8", $_ )} @{$args{'content'}} ], - ); - $ticketargs{MIMEObj} = $mimeobj; - $ticketargs{UpdateType} = $args{'updatetype'} || 'correspond'; - } - - foreach my $tag ( keys(%args) ) { - # if the tag was added later, skip it - my $orig_tag = $original_tags{$tag} or next; - if ( $orig_tag =~ /^customfield-?(\d+)$/i ) { - $ticketargs{ "CustomField-" . $1 } = $args{$tag}; - } elsif ( $orig_tag =~ /^(?:customfield|cf)-?(.+)$/i ) { - my $cf = RT::CustomField->new( $self->CurrentUser ); - $cf->LoadByName( Name => $1, Queue => $ticketargs{Queue} ); - $cf->LoadByName( Name => $1, Queue => 0 ) unless $cf->id; - next unless $cf->id; - $ticketargs{ "CustomField-" . $cf->id } = $args{$tag}; - } elsif ($orig_tag) { - my $cf = RT::CustomField->new( $self->CurrentUser ); - $cf->LoadByName( Name => $orig_tag, Queue => $ticketargs{Queue} ); - $cf->LoadByName( Name => $orig_tag, Queue => 0 ) unless $cf->id; - next unless $cf->id; - $ticketargs{ "CustomField-" . $cf->id } = $args{$tag}; - - } - } - - $self->GetDeferred( \%args, $template_id, $links, $postponed ); - - return $TicketObj, \%ticketargs; -} - - -=head2 _ParseXSVTemplate - -Parses a tab or comma delimited template. Should only ever be called by -L. - -=cut - -sub _ParseXSVTemplate { - my $self = shift; - my %args = (@_); - - use Regexp::Common qw(delimited); - my($first, $content) = split(/\r?\n/, $args{'Content'}, 2); - - my $delimiter; - if ( $first =~ /\t/ ) { - $delimiter = "\t"; - } else { - $delimiter = ','; - } - my @fields = split( /$delimiter/, $first ); - - my $delimiter_re = qr[$delimiter]; - my $justquoted = qr[$RE{quoted}]; - - # Used to generate automatic template ids - my $autoid = 1; - - LINE: - while ($content) { - $content =~ s/^(\s*\r?\n)+//; - - # Keep track of Queue and Requestor, so we can provide defaults - my $queue; - my $requestor; - - # The template for this line - my $template; - - # What column we're on - my $i = 0; - - # If the last iteration was the end of the line - my $EOL = 0; - - # The template id - my $template_id; - - COLUMN: - while (not $EOL and length $content and $content =~ s/^($justquoted|.*?)($delimiter_re|$)//smix) { - $EOL = not $2; - - # Strip off quotes, if they exist - my $value = $1; - if ( $value =~ /^$RE{delimited}{-delim=>qq{\'\"}}$/ ) { - substr( $value, 0, 1 ) = ""; - substr( $value, -1, 1 ) = ""; - } - - # What column is this? - my $field = $fields[$i++]; - next COLUMN unless $field =~ /\S/; - $field =~ s/^\s//; - $field =~ s/\s$//; - - if ( $field =~ /^id$/i ) { - # Special case if this is the ID column - if ( $value =~ /^\d+$/ ) { - $template_id = 'update-' . $value; - push @{ $self->{'update_tickets'} }, $template_id; - } elsif ( $value =~ /^#base-(\d+)$/ ) { - $template_id = 'base-' . $1; - push @{ $self->{'base_tickets'} }, $template_id; - } elsif ( $value =~ /\S/ ) { - $template_id = 'create-' . $value; - push @{ $self->{'create_tickets'} }, $template_id; - } - } else { - # Some translations - if ( $field =~ /^Body$/i - || $field =~ /^Data$/i - || $field =~ /^Message$/i ) - { - $field = 'Content'; - } elsif ( $field =~ /^Summary$/i ) { - $field = 'Subject'; - } elsif ( $field =~ /^Queue$/i ) { - # Note that we found a queue - $queue = 1; - $value ||= $args{'Queue'}; - } elsif ( $field =~ /^Requestors?$/i ) { - $field = 'Requestor'; # Remove plural - # Note that we found a requestor - $requestor = 1; - $value ||= $args{'Requestor'}; - } - - # Tack onto the end of the template - $template .= $field . ": "; - $template .= (defined $value ? $value : ""); - $template .= "\n"; - $template .= "ENDOFCONTENT\n" - if $field =~ /^Content$/i; - } - } - - # Ignore blank lines - next unless $template; - - # If we didn't find a queue of requestor, tack on the defaults - if ( !$queue && $args{'Queue'} ) { - $template .= "Queue: $args{'Queue'}\n"; - } - if ( !$requestor && $args{'Requestor'} ) { - $template .= "Requestor: $args{'Requestor'}\n"; - } - - # If we never found an ID, come up with one - unless ($template_id) { - $autoid++ while exists $self->{'templates'}->{"create-auto-$autoid"}; - $template_id = "create-auto-$autoid"; - # Also, it's a ticket to create - push @{ $self->{'create_tickets'} }, $template_id; - } - - # Save the template we generated - $self->{'templates'}->{$template_id} = $template; - - } -} - -sub GetDeferred { - my $self = shift; - my $args = shift; - my $id = shift; - my $links = shift; - my $postponed = shift; - - # Deferred processing - push @$links, - ( - $id, - { DependsOn => $args->{'dependson'}, - DependedOnBy => $args->{'dependedonby'}, - RefersTo => $args->{'refersto'}, - ReferredToBy => $args->{'referredtoby'}, - Children => $args->{'children'}, - Parents => $args->{'parents'}, - } - ); - - push @$postponed, ( - - # Status is postponed so we don't violate dependencies - $id, { Status => $args->{'status'}, } - ); -} - -sub GetUpdateTemplate { - my $self = shift; - my $t = shift; - - my $string; - $string .= "Queue: " . $t->QueueObj->Name . "\n"; - $string .= "Subject: " . $t->Subject . "\n"; - $string .= "Status: " . $t->Status . "\n"; - $string .= "UpdateType: correspond\n"; - $string .= "Content: \n"; - $string .= "ENDOFCONTENT\n"; - $string .= "Due: " . $t->DueObj->AsString . "\n"; - $string .= "Starts: " . $t->StartsObj->AsString . "\n"; - $string .= "Started: " . $t->StartedObj->AsString . "\n"; - $string .= "Resolved: " . $t->ResolvedObj->AsString . "\n"; - $string .= "Owner: " . $t->OwnerObj->Name . "\n"; - $string .= "Requestor: " . $t->RequestorAddresses . "\n"; - $string .= "Cc: " . $t->CcAddresses . "\n"; - $string .= "AdminCc: " . $t->AdminCcAddresses . "\n"; - $string .= "TimeWorked: " . $t->TimeWorked . "\n"; - $string .= "TimeEstimated: " . $t->TimeEstimated . "\n"; - $string .= "TimeLeft: " . $t->TimeLeft . "\n"; - $string .= "InitialPriority: " . $t->Priority . "\n"; - $string .= "FinalPriority: " . $t->FinalPriority . "\n"; - - foreach my $type ( sort keys %LINKTYPEMAP ) { - - # don't display duplicates - if ( $type eq "HasMember" - || $type eq "Members" - || $type eq "MemberOf" ) - { - next; - } - $string .= "$type: "; - - my $mode = $LINKTYPEMAP{$type}->{Mode}; - my $method = $LINKTYPEMAP{$type}->{Type}; - - my $links = ''; - while ( my $link = $t->$method->Next ) { - $links .= ", " if $links; - - my $object = $mode . "Obj"; - my $member = $link->$object; - $links .= $member->Id if $member; - } - $string .= $links; - $string .= "\n"; - } - - return $string; -} - -sub GetBaseTemplate { - my $self = shift; - my $t = shift; - - my $string; - $string .= "Queue: " . $t->Queue . "\n"; - $string .= "Subject: " . $t->Subject . "\n"; - $string .= "Status: " . $t->Status . "\n"; - $string .= "Due: " . $t->DueObj->Unix . "\n"; - $string .= "Starts: " . $t->StartsObj->Unix . "\n"; - $string .= "Started: " . $t->StartedObj->Unix . "\n"; - $string .= "Resolved: " . $t->ResolvedObj->Unix . "\n"; - $string .= "Owner: " . $t->Owner . "\n"; - $string .= "Requestor: " . $t->RequestorAddresses . "\n"; - $string .= "Cc: " . $t->CcAddresses . "\n"; - $string .= "AdminCc: " . $t->AdminCcAddresses . "\n"; - $string .= "TimeWorked: " . $t->TimeWorked . "\n"; - $string .= "TimeEstimated: " . $t->TimeEstimated . "\n"; - $string .= "TimeLeft: " . $t->TimeLeft . "\n"; - $string .= "InitialPriority: " . $t->Priority . "\n"; - $string .= "FinalPriority: " . $t->FinalPriority . "\n"; - - return $string; -} - -sub GetCreateTemplate { - my $self = shift; - - my $string; - - $string .= "Queue: General\n"; - $string .= "Subject: \n"; - $string .= "Status: new\n"; - $string .= "Content: \n"; - $string .= "ENDOFCONTENT\n"; - $string .= "Due: \n"; - $string .= "Starts: \n"; - $string .= "Started: \n"; - $string .= "Resolved: \n"; - $string .= "Owner: \n"; - $string .= "Requestor: \n"; - $string .= "Cc: \n"; - $string .= "AdminCc:\n"; - $string .= "TimeWorked: \n"; - $string .= "TimeEstimated: \n"; - $string .= "TimeLeft: \n"; - $string .= "InitialPriority: \n"; - $string .= "FinalPriority: \n"; - - foreach my $type ( keys %LINKTYPEMAP ) { - - # don't display duplicates - if ( $type eq "HasMember" - || $type eq 'Members' - || $type eq 'MemberOf' ) - { - next; - } - $string .= "$type: \n"; - } - return $string; -} - -sub UpdateWatchers { - my $self = shift; - my $ticket = shift; - my $args = shift; - - my @results; - - foreach my $type (qw(Requestor Cc AdminCc)) { - my $method = $type . 'Addresses'; - my $oldaddr = $ticket->$method; - - # Skip unless we have a defined field - next unless defined $args->{$type}; - my $newaddr = $args->{$type}; - - my @old = split( /,\s*/, $oldaddr ); - my @new; - for (ref $newaddr ? @{$newaddr} : split( /,\s*/, $newaddr )) { - # Sometimes these are email addresses, sometimes they're - # users. Try to guess which is which, as we want to deal - # with email addresses if at all possible. - if (/^\S+@\S+$/) { - push @new, $_; - } else { - # It doesn't look like an email address. Try to load it. - my $user = RT::User->new($self->CurrentUser); - $user->Load($_); - if ($user->Id) { - push @new, $user->EmailAddress; - } else { - push @new, $_; - } - } - } - - my %oldhash = map { $_ => 1 } @old; - my %newhash = map { $_ => 1 } @new; - - my @add = grep( !defined $oldhash{$_}, @new ); - my @delete = grep( !defined $newhash{$_}, @old ); - - foreach (@add) { - my ( $val, $msg ) = $ticket->AddWatcher( - Type => $type, - Email => $_ - ); - - push @results, - $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg; - } - - foreach (@delete) { - my ( $val, $msg ) = $ticket->DeleteWatcher( - Type => $type, - Email => $_ - ); - push @results, - $ticket->loc( "Ticket [_1]", $ticket->Id ) . ': ' . $msg; - } - } - return @results; -} - -sub UpdateCustomFields { - my $self = shift; - my $ticket = shift; - my $args = shift; - - my @results; - foreach my $arg (keys %{$args}) { - next unless $arg =~ /^CustomField-(\d+)$/; - my $cf = $1; - - my $CustomFieldObj = RT::CustomField->new($self->CurrentUser); - $CustomFieldObj->SetContextObject( $ticket ); - $CustomFieldObj->LoadById($cf); - - my @values; - if ($CustomFieldObj->Type =~ /text/i) { # Both Text and Wikitext - @values = ($args->{$arg}); - } else { - @values = split /\n/, $args->{$arg}; - } - - if ( ($CustomFieldObj->Type eq 'Freeform' - && ! $CustomFieldObj->SingleValue) || - $CustomFieldObj->Type =~ /text/i) { - foreach my $val (@values) { - $val =~ s/\r//g; - } - } - - foreach my $value (@values) { - next unless length($value); - my ( $val, $msg ) = $ticket->AddCustomFieldValue( - Field => $cf, - Value => $value - ); - push ( @results, $msg ); - } - } - return @results; -} - -sub PostProcess { - my $self = shift; - my $links = shift; - my $postponed = shift; - - # postprocessing: add links - - while ( my $template_id = shift(@$links) ) { - my $ticket = $T::Tickets{$template_id}; - $RT::Logger->debug( "Handling links for " . $ticket->Id ); - my %args = %{ shift(@$links) }; - - foreach my $type ( keys %LINKTYPEMAP ) { - next unless ( defined $args{$type} ); - foreach my $link ( - ref( $args{$type} ) ? @{ $args{$type} } : ( $args{$type} ) ) - { - next unless $link; - - if ( $link =~ /^TOP$/i ) { - $RT::Logger->debug( "Building $type link for $link: " - . $T::Tickets{TOP}->Id ); - $link = $T::Tickets{TOP}->Id; - - } elsif ( $link !~ m/^\d+$/ ) { - my $key = "create-$link"; - if ( !exists $T::Tickets{$key} ) { - $RT::Logger->debug( - "Skipping $type link for $key (non-existent)"); - next; - } - $RT::Logger->debug( "Building $type link for $link: " - . $T::Tickets{$key}->Id ); - $link = $T::Tickets{$key}->Id; - } else { - $RT::Logger->debug("Building $type link for $link"); - } - - my ( $wval, $wmsg ) = $ticket->AddLink( - Type => $LINKTYPEMAP{$type}->{'Type'}, - $LINKTYPEMAP{$type}->{'Mode'} => $link, - Silent => 1 - ); - - $RT::Logger->warning("AddLink thru $link failed: $wmsg") - unless $wval; - - # push @non_fatal_errors, $wmsg unless ($wval); - } - - } - } - - # postponed actions -- Status only, currently - while ( my $template_id = shift(@$postponed) ) { - my $ticket = $T::Tickets{$template_id}; - $RT::Logger->debug( "Handling postponed actions for " . $ticket->id ); - my %args = %{ shift(@$postponed) }; - $ticket->SetStatus( $args{Status} ) if defined $args{Status}; - } - -} - -sub Options { - my $self = shift; - my $queues = RT::Queues->new($self->CurrentUser); - $queues->UnLimit; - my @names; - while (my $queue = $queues->Next) { - push @names, $queue->Id, $queue->Name; - } - return ( - { - 'name' => 'Queue', - 'label' => 'In queue', - 'type' => 'select', - 'options' => \@names - } - ) -} - -RT::Base->_ImportOverlays(); - -1; - diff --git a/rt/lib/RT/Action/SendEmail.pm.orig b/rt/lib/RT/Action/SendEmail.pm.orig deleted file mode 100755 index 0f11cc141..000000000 --- a/rt/lib/RT/Action/SendEmail.pm.orig +++ /dev/null @@ -1,1131 +0,0 @@ -# BEGIN BPS TAGGED BLOCK {{{ -# -# COPYRIGHT: -# -# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC -# -# -# (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 }}} - -# Portions Copyright 2000 Tobias Brox - -package RT::Action::SendEmail; - -use strict; -use warnings; - -use base qw(RT::Action); - -use RT::EmailParser; -use RT::Interface::Email; -use Email::Address; -our @EMAIL_RECIPIENT_HEADERS = qw(To Cc Bcc); - - -=head1 NAME - -RT::Action::SendEmail - An Action which users can use to send mail -or can subclassed for more specialized mail sending behavior. -RT::Action::AutoReply is a good example subclass. - -=head1 SYNOPSIS - - use base 'RT::Action::SendEmail'; - -=head1 DESCRIPTION - -Basically, you create another module RT::Action::YourAction which ISA -RT::Action::SendEmail. - -=head1 METHODS - -=head2 CleanSlate - -Cleans class-wide options, like L. - -=cut - -sub CleanSlate { - my $self = shift; - $self->AttachTickets(undef); -} - -=head2 Commit - -Sends the prepared message and writes outgoing record into DB if the feature is -activated in the config. - -=cut - -sub Commit { - my $self = shift; - - return abs $self->SendMessage( $self->TemplateObj->MIMEObj ) - unless RT->Config->Get('RecordOutgoingEmail'); - - $self->DeferDigestRecipients(); - my $message = $self->TemplateObj->MIMEObj; - - my $orig_message; - $orig_message = $message->dup if RT::Interface::Email::WillSignEncrypt( - Attachment => $self->TransactionObj->Attachments->First, - Ticket => $self->TicketObj, - ); - - my ($ret) = $self->SendMessage($message); - return abs( $ret ) if $ret <= 0; - - if ($orig_message) { - $message->attach( - Type => 'application/x-rt-original-message', - Disposition => 'inline', - Data => $orig_message->as_string, - ); - } - $self->RecordOutgoingMailTransaction($message); - $self->RecordDeferredRecipients(); - return 1; -} - -=head2 Prepare - -Builds an outgoing email we're going to send using scrip's template. - -=cut - -sub Prepare { - my $self = shift; - - my ( $result, $message ) = $self->TemplateObj->Parse( - Argument => $self->Argument, - TicketObj => $self->TicketObj, - TransactionObj => $self->TransactionObj - ); - if ( !$result ) { - return (undef); - } - - my $MIMEObj = $self->TemplateObj->MIMEObj; - - # Header - $self->SetRTSpecialHeaders(); - - my %seen; - foreach my $type (@EMAIL_RECIPIENT_HEADERS) { - @{ $self->{$type} } - = grep defined && length && !$seen{ lc $_ }++, - @{ $self->{$type} }; - } - - $self->RemoveInappropriateRecipients(); - - # Go add all the Tos, Ccs and Bccs that we need to to the message to - # make it happy, but only if we actually have values in those arrays. - -# TODO: We should be pulling the recipients out of the template and shove them into To, Cc and Bcc - - for my $header (@EMAIL_RECIPIENT_HEADERS) { - - $self->SetHeader( $header, join( ', ', @{ $self->{$header} } ) ) - if (!$MIMEObj->head->get($header) - && $self->{$header} - && @{ $self->{$header} } ); - } - # PseudoTo (fake to headers) shouldn't get matched for message recipients. - # If we don't have any 'To' header (but do have other recipients), drop in - # the pseudo-to header. - $self->SetHeader( 'To', join( ', ', @{ $self->{'PseudoTo'} } ) ) - if $self->{'PseudoTo'} - && @{ $self->{'PseudoTo'} } - && !$MIMEObj->head->get('To') - && ( $MIMEObj->head->get('Cc') or $MIMEObj->head->get('Bcc') ); - - # We should never have to set the MIME-Version header - $self->SetHeader( 'MIME-Version', '1.0' ); - - # fsck.com #5959: Since RT sends 8bit mail, we should say so. - $self->SetHeader( 'Content-Transfer-Encoding', '8bit' ); - - # For security reasons, we only send out textual mails. - foreach my $part ( grep !$_->is_multipart, $MIMEObj->parts_DFS ) { - my $type = $part->mime_type || 'text/plain'; - $type = 'text/plain' unless RT::I18N::IsTextualContentType($type); - $part->head->mime_attr( "Content-Type" => $type ); - # utf-8 here is for _FindOrGuessCharset in I18N.pm - # it's not the final charset/encoding sent - $part->head->mime_attr( "Content-Type.charset" => 'utf-8' ); - } - - RT::I18N::SetMIMEEntityToEncoding( $MIMEObj, - RT->Config->Get('EmailOutputEncoding'), - 'mime_words_ok', ); - - # Build up a MIME::Entity that looks like the original message. - $self->AddAttachments if ( $MIMEObj->head->get('RT-Attach-Message') - && ( $MIMEObj->head->get('RT-Attach-Message') !~ /^(n|no|0|off|false)$/i ) ); - - $self->AddTickets; - - my $attachment = $self->TransactionObj->Attachments->First; - if ($attachment - && !( - $attachment->GetHeader('X-RT-Encrypt') - || $self->TicketObj->QueueObj->Encrypt - ) - ) - { - $attachment->SetHeader( 'X-RT-Encrypt' => 1 ) - if ( $attachment->GetHeader("X-RT-Incoming-Encryption") || '' ) eq - 'Success'; - } - - return $result; -} - -=head2 To - -Returns an array of L objects containing all the To: recipients for this notification - -=cut - -sub To { - my $self = shift; - return ( $self->AddressesFromHeader('To') ); -} - -=head2 Cc - -Returns an array of L objects containing all the Cc: recipients for this notification - -=cut - -sub Cc { - my $self = shift; - return ( $self->AddressesFromHeader('Cc') ); -} - -=head2 Bcc - -Returns an array of L objects containing all the Bcc: recipients for this notification - -=cut - -sub Bcc { - my $self = shift; - return ( $self->AddressesFromHeader('Bcc') ); - -} - -sub AddressesFromHeader { - my $self = shift; - my $field = shift; - my $header = $self->TemplateObj->MIMEObj->head->get($field); - my @addresses = Email::Address->parse($header); - - return (@addresses); -} - -=head2 SendMessage MIMEObj - -sends the message using RT's preferred API. -TODO: Break this out to a separate module - -=cut - -sub SendMessage { - - # DO NOT SHIFT @_ in this subroutine. It breaks Hook::LexWrap's - # ability to pass @_ to a 'post' routine. - my ( $self, $MIMEObj ) = @_; - - my $msgid = $MIMEObj->head->get('Message-ID'); - chomp $msgid; - - $self->ScripActionObj->{_Message_ID}++; - - $RT::Logger->info( $msgid . " #" - . $self->TicketObj->id . "/" - . $self->TransactionObj->id - . " - Scrip " - . ($self->ScripObj->id || '#rule'). " " - . ( $self->ScripObj->Description || '' ) ); - - my $status = RT::Interface::Email::SendEmail( - Entity => $MIMEObj, - Ticket => $self->TicketObj, - Transaction => $self->TransactionObj, - ); - - - return $status unless ($status > 0 || exists $self->{'Deferred'}); - - my $success = $msgid . " sent "; - foreach (@EMAIL_RECIPIENT_HEADERS) { - my $recipients = $MIMEObj->head->get($_); - $success .= " $_: " . $recipients if $recipients; - } - - if( exists $self->{'Deferred'} ) { - for (qw(daily weekly susp)) { - $success .= "\nBatched email $_ for: ". join(", ", keys %{ $self->{'Deferred'}{ $_ } } ) - if exists $self->{'Deferred'}{ $_ }; - } - } - - $success =~ s/\n//g; - - $RT::Logger->info($success); - - return (1); -} - -=head2 AddAttachments - -Takes any attachments to this transaction and attaches them to the message -we're building. - -=cut - -sub AddAttachments { - my $self = shift; - - my $MIMEObj = $self->TemplateObj->MIMEObj; - - $MIMEObj->head->delete('RT-Attach-Message'); - - my $attachments = RT::Attachments->new( RT->SystemUser ); - $attachments->Limit( - FIELD => 'TransactionId', - VALUE => $self->TransactionObj->Id - ); - - # Don't attach anything blank - $attachments->LimitNotEmpty; - $attachments->OrderBy( FIELD => 'id' ); - - # We want to make sure that we don't include the attachment that's - # being used as the "Content" of this message" unless that attachment's - # content type is not like text/... - my $transaction_content_obj = $self->TransactionObj->ContentObj; - - if ( $transaction_content_obj - && $transaction_content_obj->ContentType =~ m{text/}i ) - { - # If this was part of a multipart/alternative, skip all of the kids - my $parent = $transaction_content_obj->ParentObj; - if ($parent and $parent->Id and $parent->ContentType eq "multipart/alternative") { - $attachments->Limit( - ENTRYAGGREGATOR => 'AND', - FIELD => 'parent', - OPERATOR => '!=', - VALUE => $parent->Id, - ); - } else { - $attachments->Limit( - ENTRYAGGREGATOR => 'AND', - FIELD => 'id', - OPERATOR => '!=', - VALUE => $transaction_content_obj->Id, - ); - } - } - - # attach any of this transaction's attachments - my $seen_attachment = 0; - while ( my $attach = $attachments->Next ) { - if ( !$seen_attachment ) { - $MIMEObj->make_multipart( 'mixed', Force => 1 ); - $seen_attachment = 1; - } - $self->AddAttachment($attach); - } -} - -=head2 AddAttachment $attachment - -Takes one attachment object of L class and attaches it to the message -we're building. - -=cut - -sub AddAttachment { - my $self = shift; - my $attach = shift; - my $MIMEObj = shift || $self->TemplateObj->MIMEObj; - - # $attach->TransactionObj may not always be $self->TransactionObj - return unless $attach->Id - and $attach->TransactionObj->CurrentUserCanSee; - - # ->attach expects just the disposition type; extract it if we have the header - # or default to "attachment" - my $disp = ($attach->GetHeader('Content-Disposition') || '') - =~ /^\s*(inline|attachment)/i ? $1 : "attachment"; - - $MIMEObj->attach( - Type => $attach->ContentType, - Charset => $attach->OriginalEncoding, - Data => $attach->OriginalContent, - Disposition => $disp, - Filename => $self->MIMEEncodeString( $attach->Filename ), - 'RT-Attachment:' => $self->TicketObj->Id . "/" - . $self->TransactionObj->Id . "/" - . $attach->id, - Encoding => '-SUGGEST', - ); -} - -=head2 AttachTickets [@IDs] - -Returns or set list of ticket's IDs that should be attached to an outgoing message. - -B this method works as a class method and setup things global, so you have to -clean list by passing undef as argument. - -=cut - -{ - my $list = []; - - sub AttachTickets { - my $self = shift; - $list = [ grep defined, @_ ] if @_; - return @$list; - } -} - -=head2 AddTickets - -Attaches tickets to the current message, list of tickets' ids get from -L method. - -=cut - -sub AddTickets { - my $self = shift; - $self->AddTicket($_) foreach $self->AttachTickets; - return; -} - -=head2 AddTicket $ID - -Attaches a ticket with ID to the message. - -Each ticket is attached as multipart entity and all its messages and attachments -are attached as sub entities in order of creation, but only if transaction type -is Create or Correspond. - -=cut - -sub AddTicket { - my $self = shift; - my $tid = shift; - - my $attachs = RT::Attachments->new( $self->TransactionObj->CreatorObj ); - my $txn_alias = $attachs->TransactionAlias; - $attachs->Limit( ALIAS => $txn_alias, FIELD => 'Type', VALUE => 'Create' ); - $attachs->Limit( - ALIAS => $txn_alias, - FIELD => 'Type', - VALUE => 'Correspond' - ); - $attachs->LimitByTicket($tid); - $attachs->LimitNotEmpty; - $attachs->OrderBy( FIELD => 'Created' ); - - my $ticket_mime = MIME::Entity->build( - Type => 'multipart/mixed', - Top => 0, - Description => "ticket #$tid", - ); - while ( my $attachment = $attachs->Next ) { - $self->AddAttachment( $attachment, $ticket_mime ); - } - if ( $ticket_mime->parts ) { - my $email_mime = $self->TemplateObj->MIMEObj; - $email_mime->make_multipart; - $email_mime->add_part($ticket_mime); - } - return; -} - -=head2 RecordOutgoingMailTransaction MIMEObj - -Record a transaction in RT with this outgoing message for future record-keeping purposes - -=cut - -sub RecordOutgoingMailTransaction { - my $self = shift; - my $MIMEObj = shift; - - my @parts = $MIMEObj->parts; - my @attachments; - my @keep; - foreach my $part (@parts) { - my $attach = $part->head->get('RT-Attachment'); - if ($attach) { - $RT::Logger->debug( - "We found an attachment. we want to not record it."); - push @attachments, $attach; - } else { - $RT::Logger->debug("We found a part. we want to record it."); - push @keep, $part; - } - } - $MIMEObj->parts( \@keep ); - foreach my $attachment (@attachments) { - $MIMEObj->head->add( 'RT-Attachment', $attachment ); - } - - RT::I18N::SetMIMEEntityToEncoding( $MIMEObj, 'utf-8', 'mime_words_ok' ); - - my $transaction - = RT::Transaction->new( $self->TransactionObj->CurrentUser ); - -# XXX: TODO -> Record attachments as references to things in the attachments table, maybe. - - my $type; - if ( $self->TransactionObj->Type eq 'Comment' ) { - $type = 'CommentEmailRecord'; - } else { - $type = 'EmailRecord'; - } - - my $msgid = $MIMEObj->head->get('Message-ID'); - chomp $msgid; - - my ( $id, $msg ) = $transaction->Create( - Ticket => $self->TicketObj->Id, - Type => $type, - Data => $msgid, - MIMEObj => $MIMEObj, - ActivateScrips => 0 - ); - - if ($id) { - $self->{'OutgoingMailTransaction'} = $id; - } else { - $RT::Logger->warning( - "Could not record outgoing message transaction: $msg"); - } - return $id; -} - -=head2 SetRTSpecialHeaders - -This routine adds all the random headers that RT wants in a mail message -that don't matter much to anybody else. - -=cut - -sub SetRTSpecialHeaders { - my $self = shift; - - $self->SetSubject(); - $self->SetSubjectToken(); - $self->SetHeaderAsEncoding( 'Subject', - RT->Config->Get('EmailOutputEncoding') ) - if ( RT->Config->Get('EmailOutputEncoding') ); - $self->SetReturnAddress(); - $self->SetReferencesHeaders(); - - unless ( $self->TemplateObj->MIMEObj->head->get('Message-ID') ) { - - # Get Message-ID for this txn - my $msgid = ""; - if ( my $msg = $self->TransactionObj->Message->First ) { - $msgid = $msg->GetHeader("RT-Message-ID") - || $msg->GetHeader("Message-ID"); - } - - # If there is one, and we can parse it, then base our Message-ID on it - if ( $msgid - and $msgid - =~ s/<(rt-.*?-\d+-\d+)\.(\d+)-\d+-\d+\@\QRT->Config->Get('Organization')\E>$/ - "<$1." . $self->TicketObj->id - . "-" . $self->ScripObj->id - . "-" . $self->ScripActionObj->{_Message_ID} - . "@" . RT->Config->Get('Organization') . ">"/eg - and $2 == $self->TicketObj->id - ) - { - $self->SetHeader( "Message-ID" => $msgid ); - } else { - $self->SetHeader( - 'Message-ID' => RT::Interface::Email::GenMessageId( - Ticket => $self->TicketObj, - Scrip => $self->ScripObj, - ScripAction => $self->ScripActionObj - ), - ); - } - } - - if (my $precedence = RT->Config->Get('DefaultMailPrecedence') - and !$self->TemplateObj->MIMEObj->head->get("Precedence") - ) { - $self->SetHeader( 'Precedence', $precedence ); - } - - $self->SetHeader( 'X-RT-Loop-Prevention', RT->Config->Get('rtname') ); - $self->SetHeader( 'RT-Ticket', - RT->Config->Get('rtname') . " #" . $self->TicketObj->id() ); - $self->SetHeader( 'Managed-by', - "RT $RT::VERSION (http://www.bestpractical.com/rt/)" ); - -# XXX, TODO: use /ShowUser/ShowUserEntry(or something like that) when it would be -# refactored into user's method. - if ( my $email = $self->TransactionObj->CreatorObj->EmailAddress - and RT->Config->Get('UseOriginatorHeader') - ) { - $self->SetHeader( 'RT-Originator', $email ); - } - -} - - -sub DeferDigestRecipients { - my $self = shift; - $RT::Logger->debug( "Calling SetRecipientDigests for transaction " . $self->TransactionObj . ", id " . $self->TransactionObj->id ); - - # The digest attribute will be an array of notifications that need to - # be sent for this transaction. The array will have the following - # format for its objects. - # $digest_hash -> {daily|weekly|susp} -> address -> {To|Cc|Bcc} - # -> sent -> {true|false} - # The "sent" flag will be used by the cron job to indicate that it has - # run on this transaction. - # In a perfect world we might move this hash construction to the - # extension module itself. - my $digest_hash = {}; - - foreach my $mailfield (@EMAIL_RECIPIENT_HEADERS) { - # If we have a "PseudoTo", the "To" contains it, so we don't need to access it - next if ( ( $self->{'PseudoTo'} && @{ $self->{'PseudoTo'} } ) && ( $mailfield eq 'To' ) ); - $RT::Logger->debug( "Working on mailfield $mailfield; recipients are " . join( ',', @{ $self->{$mailfield} } ) ); - - # Store the 'daily digest' folk in an array. - my ( @send_now, @daily_digest, @weekly_digest, @suspended ); - - # Have to get the list of addresses directly from the MIME header - # at this point. - $RT::Logger->debug( $self->TemplateObj->MIMEObj->head->as_string ); - foreach my $rcpt ( map { $_->address } $self->AddressesFromHeader($mailfield) ) { - next unless $rcpt; - my $user_obj = RT::User->new(RT->SystemUser); - $user_obj->LoadByEmail($rcpt); - if ( ! $user_obj->id ) { - # If there's an email address in here without an associated - # RT user, pass it on through. - $RT::Logger->debug( "User $rcpt is not associated with an RT user object. Send mail."); - push( @send_now, $rcpt ); - next; - } - - my $mailpref = RT->Config->Get( 'EmailFrequency', $user_obj ) || ''; - $RT::Logger->debug( "Got user mail preference '$mailpref' for user $rcpt"); - - if ( $mailpref =~ /daily/i ) { push( @daily_digest, $rcpt ) } - elsif ( $mailpref =~ /weekly/i ) { push( @weekly_digest, $rcpt ) } - elsif ( $mailpref =~ /suspend/i ) { push( @suspended, $rcpt ) } - else { push( @send_now, $rcpt ) } - } - - # Reset the relevant mail field. - $RT::Logger->debug( "Removing deferred recipients from $mailfield: line"); - if (@send_now) { - $self->SetHeader( $mailfield, join( ', ', @send_now ) ); - } else { # No recipients! Remove the header. - $self->TemplateObj->MIMEObj->head->delete($mailfield); - } - - # Push the deferred addresses into the appropriate field in - # our attribute hash, with the appropriate mail header. - $RT::Logger->debug( - "Setting deferred recipients for attribute creation"); - $digest_hash->{'daily'}->{$_} = {'header' => $mailfield , _sent => 0} for (@daily_digest); - $digest_hash->{'weekly'}->{$_} ={'header' => $mailfield, _sent => 0} for (@weekly_digest); - $digest_hash->{'susp'}->{$_} = {'header' => $mailfield, _sent =>0 } for (@suspended); - } - - if ( scalar keys %$digest_hash ) { - - # Save the hash so that we can add it as an attribute to the - # outgoing email transaction. - $self->{'Deferred'} = $digest_hash; - } else { - $RT::Logger->debug( "No recipients found for deferred delivery on " - . "transaction #" - . $self->TransactionObj->id ); - } -} - - - -sub RecordDeferredRecipients { - my $self = shift; - return unless exists $self->{'Deferred'}; - - my $txn_id = $self->{'OutgoingMailTransaction'}; - return unless $txn_id; - - my $txn_obj = RT::Transaction->new( $self->CurrentUser ); - $txn_obj->Load( $txn_id ); - my( $ret, $msg ) = $txn_obj->AddAttribute( - Name => 'DeferredRecipients', - Content => $self->{'Deferred'} - ); - $RT::Logger->warning( "Unable to add deferred recipients to outgoing transaction: $msg" ) - unless $ret; - - return ($ret,$msg); -} - -=head2 SquelchMailTo - -Returns list of the addresses to squelch on this transaction. - -=cut - -sub SquelchMailTo { - my $self = shift; - return map $_->Content, $self->TransactionObj->SquelchMailTo; -} - -=head2 RemoveInappropriateRecipients - -Remove addresses that are RT addresses or that are on this transaction's blacklist - -=cut - -sub RemoveInappropriateRecipients { - my $self = shift; - - my @blacklist = (); - - # If there are no recipients, don't try to send the message. - # If the transaction has content and has the header RT-Squelch-Replies-To - - my $msgid = $self->TemplateObj->MIMEObj->head->get('Message-Id'); - if ( my $attachment = $self->TransactionObj->Attachments->First ) { - - if ( $attachment->GetHeader('RT-DetectedAutoGenerated') ) { - - # What do we want to do with this? It's probably (?) a bounce - # caused by one of the watcher addresses being broken. - # Default ("true") is to redistribute, for historical reasons. - - if ( !RT->Config->Get('RedistributeAutoGeneratedMessages') ) { - - # Don't send to any watchers. - @{ $self->{$_} } = () for (@EMAIL_RECIPIENT_HEADERS); - $RT::Logger->info( $msgid - . " The incoming message was autogenerated. " - . "Not redistributing this message based on site configuration." - ); - } elsif ( RT->Config->Get('RedistributeAutoGeneratedMessages') eq - 'privileged' ) - { - - # Only send to "privileged" watchers. - foreach my $type (@EMAIL_RECIPIENT_HEADERS) { - foreach my $addr ( @{ $self->{$type} } ) { - my $user = RT::User->new(RT->SystemUser); - $user->LoadByEmail($addr); - push @blacklist, $addr unless $user->id && $user->Privileged; - } - } - $RT::Logger->info( $msgid - . " The incoming message was autogenerated. " - . "Not redistributing this message to unprivileged users based on site configuration." - ); - } - } - - if ( my $squelch = $attachment->GetHeader('RT-Squelch-Replies-To') ) { - push @blacklist, split( /,/, $squelch ); - } - } - - # Let's grab the SquelchMailTo attributes and push those entries into the @blacklisted - push @blacklist, map $_->Content, $self->TicketObj->SquelchMailTo, $self->TransactionObj->SquelchMailTo; - - # Cycle through the people we're sending to and pull out anyone on the - # system blacklist - - # Trim leading and trailing spaces. - @blacklist = map { RT::User->CanonicalizeEmailAddress( $_->address ) } - Email::Address->parse( join ', ', grep defined, @blacklist ); - - foreach my $type (@EMAIL_RECIPIENT_HEADERS) { - my @addrs; - foreach my $addr ( @{ $self->{$type} } ) { - - # Weed out any RT addresses. We really don't want to talk to ourselves! - # If we get a reply back, that means it's not an RT address - if ( !RT::EmailParser->CullRTAddresses($addr) ) { - $RT::Logger->info( $msgid . "$addr appears to point to this RT instance. Skipping" ); - next; - } - if ( grep $addr eq $_, @blacklist ) { - $RT::Logger->info( $msgid . "$addr was blacklisted for outbound mail on this transaction. Skipping"); - next; - } - push @addrs, $addr; - } - foreach my $addr ( @{ $self->{'NoSquelch'}{$type} || [] } ) { - # never send email to itself - if ( !RT::EmailParser->CullRTAddresses($addr) ) { - $RT::Logger->info( $msgid . "$addr appears to point to this RT instance. Skipping" ); - next; - } - push @addrs, $addr; - } - @{ $self->{$type} } = @addrs; - } -} - -=head2 SetReturnAddress is_comment => BOOLEAN - -Calculate and set From and Reply-To headers based on the is_comment flag. - -=cut - -sub SetReturnAddress { - - my $self = shift; - my %args = ( - is_comment => 0, - friendly_name => undef, - @_ - ); - - # From and Reply-To - # $args{is_comment} should be set if the comment address is to be used. - my $replyto; - - if ( $args{'is_comment'} ) { - $replyto = $self->TicketObj->QueueObj->CommentAddress - || RT->Config->Get('CommentAddress'); - } else { - $replyto = $self->TicketObj->QueueObj->CorrespondAddress - || RT->Config->Get('CorrespondAddress'); - } - - unless ( $self->TemplateObj->MIMEObj->head->get('From') ) { - $self->SetFrom( %args, From => $replyto ); - } - - unless ( $self->TemplateObj->MIMEObj->head->get('Reply-To') ) { - $self->SetHeader( 'Reply-To', "$replyto" ); - } - -} - -=head2 SetFrom ( From => emailaddress ) - -Set the From: address for outgoing email - -=cut - -sub SetFrom { - my $self = shift; - my %args = @_; - - my $from = $args{From}; - - if ( RT->Config->Get('UseFriendlyFromLine') ) { - my $friendly_name = $self->GetFriendlyName(%args); - $from = - sprintf( - RT->Config->Get('FriendlyFromLineFormat'), - $self->MIMEEncodeString( - $friendly_name, RT->Config->Get('EmailOutputEncoding') - ), - $args{From} - ); - } - - $self->SetHeader( 'From', $from ); - - #also set Sender:, otherwise MTAs add a nonsensical value like rt@machine, - #and then Outlook prepends "rt@machine on behalf of" to the From: header - $self->SetHeader( 'Sender', $from ); -} - -=head2 GetFriendlyName - -Calculate the proper Friendly Name based on the creator of the transaction - -=cut - -sub GetFriendlyName { - my $self = shift; - my %args = ( - is_comment => 0, - friendly_name => '', - @_ - ); - my $friendly_name = $args{friendly_name}; - - unless ( $friendly_name ) { - $friendly_name = $self->TransactionObj->CreatorObj->FriendlyName; - if ( $friendly_name =~ /^"(.*)"$/ ) { # a quoted string - $friendly_name = $1; - } - } - - $friendly_name =~ s/"/\\"/g; - return $friendly_name; - -} - -=head2 SetHeader FIELD, VALUE - -Set the FIELD of the current MIME object into VALUE. - -=cut - -sub SetHeader { - my $self = shift; - my $field = shift; - my $val = shift; - - chomp $val; - chomp $field; - my $head = $self->TemplateObj->MIMEObj->head; - $head->fold_length( $field, 10000 ); - $head->replace( $field, $val ); - return $head->get($field); -} - -=head2 SetSubject - -This routine sets the subject. it does not add the rt tag. That gets done elsewhere -If subject is already defined via template, it uses that. otherwise, it tries to get -the transaction's subject. - -=cut - -sub SetSubject { - my $self = shift; - my $subject; - - if ( $self->TemplateObj->MIMEObj->head->get('Subject') ) { - return (); - } - - # don't use Transaction->Attachments because it caches - # and anything which later calls ->Attachments will be hurt - # by our RowsPerPage() call. caching is hard. - my $message = RT::Attachments->new( $self->CurrentUser ); - $message->Limit( FIELD => 'TransactionId', VALUE => $self->TransactionObj->id); - $message->OrderBy( FIELD => 'id', ORDER => 'ASC' ); - $message->RowsPerPage(1); - - if ( $self->{'Subject'} ) { - $subject = $self->{'Subject'}; - } elsif ( my $first = $message->First ) { - my $tmp = $first->GetHeader('Subject'); - $subject = defined $tmp ? $tmp : $self->TicketObj->Subject; - } else { - $subject = $self->TicketObj->Subject; - } - $subject = '' unless defined $subject; - chomp $subject; - - $subject =~ s/(\r\n|\n|\s)/ /g; - - $self->SetHeader( 'Subject', Encode::encode_utf8( $subject ) ); - -} - -=head2 SetSubjectToken - -This routine fixes the RT tag in the subject. It's unlikely that you want to overwrite this. - -=cut - -sub SetSubjectToken { - my $self = shift; - - my $head = $self->TemplateObj->MIMEObj->head; - $self->SetHeader( - Subject => - Encode::encode_utf8( - RT::Interface::Email::AddSubjectTag( - Encode::decode_utf8( $head->get('Subject') ), - $self->TicketObj, - ), - ), - ); -} - -=head2 SetReferencesHeaders - -Set References and In-Reply-To headers for this message. - -=cut - -sub SetReferencesHeaders { - my $self = shift; - - my $top = $self->TransactionObj->Message->First; - unless ( $top ) { - $self->SetHeader( References => $self->PseudoReference ); - return (undef); - } - - my @in_reply_to = split( /\s+/m, $top->GetHeader('In-Reply-To') || '' ); - my @references = split( /\s+/m, $top->GetHeader('References') || '' ); - my @msgid = split( /\s+/m, $top->GetHeader('Message-ID') || '' ); - - # There are two main cases -- this transaction was created with - # the RT Web UI, and hence we want to *not* append its Message-ID - # to the References and In-Reply-To. OR it came from an outside - # source, and we should treat it as per the RFC - my $org = RT->Config->Get('Organization'); - if ( "@msgid" =~ /<(rt-.*?-\d+-\d+)\.(\d+)-0-0\@\Q$org\E>/ ) { - - # Make all references which are internal be to version which we - # have sent out - - for ( @references, @in_reply_to ) { - s/<(rt-.*?-\d+-\d+)\.(\d+-0-0)\@\Q$org\E>$/ - "<$1." . $self->TicketObj->id . - "-" . $self->ScripObj->id . - "-" . $self->ScripActionObj->{_Message_ID} . - "@" . $org . ">"/eg - } - - # In reply to whatever the internal message was in reply to - $self->SetHeader( 'In-Reply-To', join( " ", (@in_reply_to) ) ); - - # Default the references to whatever we're in reply to - @references = @in_reply_to unless @references; - - # References are unchanged from internal - } else { - - # In reply to that message - $self->SetHeader( 'In-Reply-To', join( " ", (@msgid) ) ); - - # Default the references to whatever we're in reply to - @references = @in_reply_to unless @references; - - # Push that message onto the end of the references - push @references, @msgid; - } - - # Push pseudo-ref to the front - my $pseudo_ref = $self->PseudoReference; - @references = ( $pseudo_ref, grep { $_ ne $pseudo_ref } @references ); - - # If there are more than 10 references headers, remove all but the - # first four and the last six (Gotta keep this from growing - # forever) - splice( @references, 4, -6 ) if ( $#references >= 10 ); - - # Add on the references - $self->SetHeader( 'References', join( " ", @references ) ); - $self->TemplateObj->MIMEObj->head->fold_length( 'References', 80 ); - -} - -=head2 PseudoReference - -Returns a fake Message-ID: header for the ticket to allow a base level of threading - -=cut - -sub PseudoReference { - - my $self = shift; - my $pseudo_ref - = 'TicketObj->id . '@' - . RT->Config->Get('Organization') . '>'; - return $pseudo_ref; -} - -=head2 SetHeaderAsEncoding($field_name, $charset_encoding) - -This routine converts the field into specified charset encoding. - -=cut - -sub SetHeaderAsEncoding { - my $self = shift; - my ( $field, $enc ) = ( shift, shift ); - - my $head = $self->TemplateObj->MIMEObj->head; - - if ( lc($field) eq 'from' and RT->Config->Get('SMTPFrom') ) { - $head->replace( $field, RT->Config->Get('SMTPFrom') ); - return; - } - - my $value = $head->get( $field ); - $value = $self->MIMEEncodeString( $value, $enc ); - $head->replace( $field, $value ); - -} - -=head2 MIMEEncodeString - -Takes a perl string and optional encoding pass it over -L. - -Basicly encode a string using B encoding according to RFC2047. - -=cut - -sub MIMEEncodeString { - my $self = shift; - return RT::Interface::Email::EncodeToMIME( String => $_[0], Charset => $_[1] ); -} - -RT::Base->_ImportOverlays(); - -1; - diff --git a/rt/lib/RT/Interface/Email.pm.orig b/rt/lib/RT/Interface/Email.pm.orig deleted file mode 100755 index 74120ba07..000000000 --- a/rt/lib/RT/Interface/Email.pm.orig +++ /dev/null @@ -1,1944 +0,0 @@ -# BEGIN BPS TAGGED BLOCK {{{ -# -# COPYRIGHT: -# -# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC -# -# -# (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; - -use strict; -use warnings; - -use Email::Address; -use MIME::Entity; -use RT::EmailParser; -use File::Temp; -use UNIVERSAL::require; -use Mail::Mailer (); -use Text::ParseWords qw/shellwords/; - -BEGIN { - use base 'Exporter'; - use vars qw ( @EXPORT_OK); - - # set the version for version checking - our $VERSION = 2.0; - - # your exported package globals go here, - # as well as any optionally exported functions - @EXPORT_OK = qw( - &CreateUser - &GetMessageContent - &CheckForLoops - &CheckForSuspiciousSender - &CheckForAutoGenerated - &CheckForBounce - &MailError - &ParseCcAddressesFromHead - &ParseSenderAddressFromHead - &ParseErrorsToAddressFromHead - &ParseAddressFromHeader - &Gateway); - -} - -=head1 NAME - - RT::Interface::Email - helper functions for parsing email sent to RT - -=head1 SYNOPSIS - - use lib "!!RT_LIB_PATH!!"; - use lib "!!RT_ETC_PATH!!"; - - use RT::Interface::Email qw(Gateway CreateUser); - -=head1 DESCRIPTION - - - - -=head1 METHODS - -=head2 CheckForLoops HEAD - -Takes a HEAD object of L class and returns true if the -message's been sent by this RT instance. Uses "X-RT-Loop-Prevention" -field of the head for test. - -=cut - -sub CheckForLoops { - my $head = shift; - - # If this instance of RT sent it our, we don't want to take it in - my $RTLoop = $head->get("X-RT-Loop-Prevention") || ""; - chomp ($RTLoop); # remove that newline - if ( $RTLoop eq RT->Config->Get('rtname') ) { - return 1; - } - - # TODO: We might not trap the case where RT instance A sends a mail - # to RT instance B which sends a mail to ... - return undef; -} - -=head2 CheckForSuspiciousSender HEAD - -Takes a HEAD object of L class and returns true if sender -is suspicious. Suspicious means mailer daemon. - -See also L. - -=cut - -sub CheckForSuspiciousSender { - my $head = shift; - - #if it's from a postmaster or mailer daemon, it's likely a bounce. - - #TODO: better algorithms needed here - there is no standards for - #bounces, so it's very difficult to separate them from anything - #else. At the other hand, the Return-To address is only ment to be - #used as an error channel, we might want to put up a separate - #Return-To address which is treated differently. - - #TODO: search through the whole email and find the right Ticket ID. - - my ( $From, $junk ) = ParseSenderAddressFromHead($head); - - # If unparseable (non-ASCII), $From can come back undef - return undef if not defined $From; - - if ( ( $From =~ /^mailer-daemon\@/i ) - or ( $From =~ /^postmaster\@/i ) - or ( $From eq "" )) - { - return (1); - - } - - return undef; -} - -=head2 CheckForAutoGenerated HEAD - -Takes a HEAD object of L class and returns true if message -is autogenerated. Checks 'Precedence' and 'X-FC-Machinegenerated' -fields of the head in tests. - -=cut - -sub CheckForAutoGenerated { - my $head = shift; - - my $Precedence = $head->get("Precedence") || ""; - if ( $Precedence =~ /^(bulk|junk)/i ) { - return (1); - } - - # Per RFC3834, any Auto-Submitted header which is not "no" means - # it is auto-generated. - my $AutoSubmitted = $head->get("Auto-Submitted") || ""; - if ( length $AutoSubmitted and $AutoSubmitted ne "no" ) { - return (1); - } - - # First Class mailer uses this as a clue. - my $FCJunk = $head->get("X-FC-Machinegenerated") || ""; - if ( $FCJunk =~ /^true/i ) { - return (1); - } - - return (0); -} - - -sub CheckForBounce { - my $head = shift; - - my $ReturnPath = $head->get("Return-path") || ""; - return ( $ReturnPath =~ /<>/ ); -} - - -=head2 MailError PARAM HASH - -Sends an error message. Takes a param hash: - -=over 4 - -=item From - sender's address, by default is 'CorrespondAddress'; - -=item To - recipient, by default is 'OwnerEmail'; - -=item Bcc - optional Bcc recipients; - -=item Subject - subject of the message, default is 'There has been an error'; - -=item Explanation - main content of the error, default value is 'Unexplained error'; - -=item MIMEObj - optional MIME entity that's attached to the error mail, as well we -add 'In-Reply-To' field to the error that points to this message. - -=item Attach - optional text that attached to the error as 'message/rfc822' part. - -=item LogLevel - log level under which we should write the subject and -explanation message into the log, by default we log it as critical. - -=back - -=cut - -sub MailError { - my %args = ( - To => RT->Config->Get('OwnerEmail'), - Bcc => undef, - From => RT->Config->Get('CorrespondAddress'), - Subject => 'There has been an error', - Explanation => 'Unexplained error', - MIMEObj => undef, - Attach => undef, - LogLevel => 'crit', - @_ - ); - - $RT::Logger->log( - level => $args{'LogLevel'}, - message => "$args{Subject}: $args{'Explanation'}", - ) if $args{'LogLevel'}; - - # the colons are necessary to make ->build include non-standard headers - my %entity_args = ( - Type => "multipart/mixed", - From => $args{'From'}, - Bcc => $args{'Bcc'}, - To => $args{'To'}, - Subject => $args{'Subject'}, - 'X-RT-Loop-Prevention:' => RT->Config->Get('rtname'), - ); - - # only set precedence if the sysadmin wants us to - if (defined(RT->Config->Get('DefaultErrorMailPrecedence'))) { - $entity_args{'Precedence:'} = RT->Config->Get('DefaultErrorMailPrecedence'); - } - - my $entity = MIME::Entity->build(%entity_args); - SetInReplyTo( Message => $entity, InReplyTo => $args{'MIMEObj'} ); - - $entity->attach( Data => $args{'Explanation'} . "\n" ); - - if ( $args{'MIMEObj'} ) { - $args{'MIMEObj'}->sync_headers; - $entity->add_part( $args{'MIMEObj'} ); - } - - if ( $args{'Attach'} ) { - $entity->attach( Data => $args{'Attach'}, Type => 'message/rfc822' ); - - } - - SendEmail( Entity => $entity, Bounce => 1 ); -} - - -=head2 SendEmail Entity => undef, [ Bounce => 0, Ticket => undef, Transaction => undef ] - -Sends an email (passed as a L object C) using -RT's outgoing mail configuration. If C is passed, and is a -true value, the message will be marked as an autogenerated error, if -possible. Sets Date field of the head to now if it's not set. - -If the C header is set to any true value, the mail will -not be sent. One use is to let extensions easily cancel outgoing mail. - -Ticket and Transaction arguments are optional. If Transaction is -specified and Ticket is not then ticket of the transaction is -used, but only if the transaction belongs to a ticket. - -Returns 1 on success, 0 on error or -1 if message has no recipients -and hasn't been sent. - -=head3 Signing and Encrypting - -This function as well signs and/or encrypts the message according to -headers of a transaction's attachment or properties of a ticket's queue. -To get full access to the configuration Ticket and/or Transaction -arguments must be provided, but you can force behaviour using Sign -and/or Encrypt arguments. - -The following precedence of arguments are used to figure out if -the message should be encrypted and/or signed: - -* if Sign or Encrypt argument is defined then its value is used - -* else if Transaction's first attachment has X-RT-Sign or X-RT-Encrypt -header field then it's value is used - -* else properties of a queue of the Ticket are used. - -=cut - -sub WillSignEncrypt { - my %args = @_; - my $attachment = delete $args{Attachment}; - my $ticket = delete $args{Ticket}; - - if ( not RT->Config->Get('GnuPG')->{'Enable'} ) { - $args{Sign} = $args{Encrypt} = 0; - return wantarray ? %args : 0; - } - - for my $argument ( qw(Sign Encrypt) ) { - next if defined $args{ $argument }; - - if ( $attachment and defined $attachment->GetHeader("X-RT-$argument") ) { - $args{$argument} = $attachment->GetHeader("X-RT-$argument"); - } elsif ( $ticket and $argument eq "Encrypt" ) { - $args{Encrypt} = $ticket->QueueObj->Encrypt(); - } elsif ( $ticket and $argument eq "Sign" ) { - # Note that $queue->Sign is UI-only, and that all - # UI-generated messages explicitly set the X-RT-Crypt header - # to 0 or 1; thus this path is only taken for messages - # generated _not_ via the web UI. - $args{Sign} = $ticket->QueueObj->SignAuto(); - } - } - - return wantarray ? %args : ($args{Sign} || $args{Encrypt}); -} - -sub SendEmail { - my (%args) = ( - Entity => undef, - Bounce => 0, - Ticket => undef, - Transaction => undef, - @_, - ); - - my $TicketObj = $args{'Ticket'}; - my $TransactionObj = $args{'Transaction'}; - - foreach my $arg( qw(Entity Bounce) ) { - next unless defined $args{ lc $arg }; - - $RT::Logger->warning("'". lc($arg) ."' argument is deprecated, use '$arg' instead"); - $args{ $arg } = delete $args{ lc $arg }; - } - - unless ( $args{'Entity'} ) { - $RT::Logger->crit( "Could not send mail without 'Entity' object" ); - return 0; - } - - my $msgid = $args{'Entity'}->head->get('Message-ID') || ''; - chomp $msgid; - - # If we don't have any recipients to send to, don't send a message; - unless ( $args{'Entity'}->head->get('To') - || $args{'Entity'}->head->get('Cc') - || $args{'Entity'}->head->get('Bcc') ) - { - $RT::Logger->info( $msgid . " No recipients found. Not sending." ); - return -1; - } - - if ($args{'Entity'}->head->get('X-RT-Squelch')) { - $RT::Logger->info( $msgid . " Squelch header found. Not sending." ); - return -1; - } - - if ( $TransactionObj && !$TicketObj - && $TransactionObj->ObjectType eq 'RT::Ticket' ) - { - $TicketObj = $TransactionObj->Object; - } - - if ( RT->Config->Get('GnuPG')->{'Enable'} ) { - %args = WillSignEncrypt( - %args, - Attachment => $TransactionObj ? $TransactionObj->Attachments->First : undef, - Ticket => $TicketObj, - ); - my $res = SignEncrypt( %args ); - return $res unless $res > 0; - } - - unless ( $args{'Entity'}->head->get('Date') ) { - require RT::Date; - my $date = RT::Date->new( RT->SystemUser ); - $date->SetToNow; - $args{'Entity'}->head->set( 'Date', $date->RFC2822( Timezone => 'server' ) ); - } - - my $mail_command = RT->Config->Get('MailCommand'); - - if ($mail_command eq 'testfile' and not $Mail::Mailer::testfile::config{outfile}) { - $Mail::Mailer::testfile::config{outfile} = File::Temp->new; - $RT::Logger->info("Storing outgoing emails in $Mail::Mailer::testfile::config{outfile}"); - } - - # if it is a sub routine, we just return it; - return $mail_command->($args{'Entity'}) if UNIVERSAL::isa( $mail_command, 'CODE' ); - - if ( $mail_command eq 'sendmailpipe' ) { - my $path = RT->Config->Get('SendmailPath'); - my @args = shellwords(RT->Config->Get('SendmailArguments')); - - # SetOutgoingMailFrom and bounces conflict, since they both want -f - if ( $args{'Bounce'} ) { - push @args, shellwords(RT->Config->Get('SendmailBounceArguments')); - } elsif ( my $MailFrom = RT->Config->Get('SetOutgoingMailFrom') ) { - my $OutgoingMailAddress = $MailFrom =~ /\@/ ? $MailFrom : undef; - my $Overrides = RT->Config->Get('OverrideOutgoingMailFrom') || {}; - - if ($TicketObj) { - my $QueueName = $TicketObj->QueueObj->Name; - my $QueueAddressOverride = $Overrides->{$QueueName}; - - if ($QueueAddressOverride) { - $OutgoingMailAddress = $QueueAddressOverride; - } else { - $OutgoingMailAddress ||= $TicketObj->QueueObj->CorrespondAddress - || RT->Config->Get('CorrespondAddress'); - } - } - elsif ($Overrides->{'Default'}) { - $OutgoingMailAddress = $Overrides->{'Default'}; - } - - push @args, "-f", $OutgoingMailAddress - if $OutgoingMailAddress; - } - - # VERP - if ( $TransactionObj and - my $prefix = RT->Config->Get('VERPPrefix') and - my $domain = RT->Config->Get('VERPDomain') ) - { - my $from = $TransactionObj->CreatorObj->EmailAddress; - $from =~ s/@/=/g; - $from =~ s/\s//g; - push @args, "-f", "$prefix$from\@$domain"; - } - - eval { - # don't ignore CHLD signal to get proper exit code - local $SIG{'CHLD'} = 'DEFAULT'; - - # if something wrong with $mail->print we will get PIPE signal, handle it - local $SIG{'PIPE'} = sub { die "program unexpectedly closed pipe" }; - - require IPC::Open2; - my ($mail, $stdout); - my $pid = IPC::Open2::open2( $stdout, $mail, $path, @args ) - or die "couldn't execute program: $!"; - - $args{'Entity'}->print($mail); - close $mail or die "close pipe failed: $!"; - - waitpid($pid, 0); - if ($?) { - # sendmail exit statuses mostly errors with data not software - # TODO: status parsing: core dump, exit on signal or EX_* - my $msg = "$msgid: `$path @args` exited with code ". ($?>>8); - $msg = ", interrupted by signal ". ($?&127) if $?&127; - $RT::Logger->error( $msg ); - die $msg; - } - }; - if ( $@ ) { - $RT::Logger->crit( "$msgid: Could not send mail with command `$path @args`: " . $@ ); - if ( $TicketObj ) { - _RecordSendEmailFailure( $TicketObj ); - } - return 0; - } - } - elsif ( $mail_command eq 'smtp' ) { - require Net::SMTP; - my $smtp = do { local $@; eval { Net::SMTP->new( - Host => RT->Config->Get('SMTPServer'), - Debug => RT->Config->Get('SMTPDebug'), - ) } }; - unless ( $smtp ) { - $RT::Logger->crit( "Could not connect to SMTP server."); - if ($TicketObj) { - _RecordSendEmailFailure( $TicketObj ); - } - return 0; - } - - # duplicate head as we want drop Bcc field - my $head = $args{'Entity'}->head->dup; - my @recipients = map $_->address, map - Email::Address->parse($head->get($_)), qw(To Cc Bcc); - $head->delete('Bcc'); - - my $sender = RT->Config->Get('SMTPFrom') - || $args{'Entity'}->head->get('From'); - chomp $sender; - - my $status = $smtp->mail( $sender ) - && $smtp->recipient( @recipients ); - - if ( $status ) { - $smtp->data; - my $fh = $smtp->tied_fh; - $head->print( $fh ); - print $fh "\n"; - $args{'Entity'}->print_body( $fh ); - $smtp->dataend; - } - $smtp->quit; - - unless ( $status ) { - $RT::Logger->crit( "$msgid: Could not send mail via SMTP." ); - if ( $TicketObj ) { - _RecordSendEmailFailure( $TicketObj ); - } - return 0; - } - } - else { - local ($ENV{'MAILADDRESS'}, $ENV{'PERL_MAILERS'}); - - my @mailer_args = ($mail_command); - if ( $mail_command eq 'sendmail' ) { - $ENV{'PERL_MAILERS'} = RT->Config->Get('SendmailPath'); - push @mailer_args, split(/\s+/, RT->Config->Get('SendmailArguments')); - } - else { - push @mailer_args, RT->Config->Get('MailParams'); - } - - unless ( $args{'Entity'}->send( @mailer_args ) ) { - $RT::Logger->crit( "$msgid: Could not send mail." ); - if ( $TicketObj ) { - _RecordSendEmailFailure( $TicketObj ); - } - return 0; - } - } - return 1; -} - -=head2 PrepareEmailUsingTemplate Template => '', Arguments => {} - -Loads a template. Parses it using arguments if it's not empty. -Returns a tuple (L object, error message). - -Note that even if a template object is returned MIMEObj method -may return undef for empty templates. - -=cut - -sub PrepareEmailUsingTemplate { - my %args = ( - Template => '', - Arguments => {}, - @_ - ); - - my $template = RT::Template->new( RT->SystemUser ); - $template->LoadGlobalTemplate( $args{'Template'} ); - unless ( $template->id ) { - return (undef, "Couldn't load template '". $args{'Template'} ."'"); - } - return $template if $template->IsEmpty; - - my ($status, $msg) = $template->Parse( %{ $args{'Arguments'} } ); - return (undef, $msg) unless $status; - - return $template; -} - -=head2 SendEmailUsingTemplate Template => '', Arguments => {}, From => CorrespondAddress, To => '', Cc => '', Bcc => '' - -Sends email using a template, takes name of template, arguments for it and recipients. - -=cut - -sub SendEmailUsingTemplate { - my %args = ( - Template => '', - Arguments => {}, - To => undef, - Cc => undef, - Bcc => undef, - From => RT->Config->Get('CorrespondAddress'), - InReplyTo => undef, - ExtraHeaders => {}, - @_ - ); - - my ($template, $msg) = PrepareEmailUsingTemplate( %args ); - return (0, $msg) unless $template; - - my $mail = $template->MIMEObj; - unless ( $mail ) { - $RT::Logger->info("Message is not sent as template #". $template->id ." is empty"); - return -1; - } - - $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 ); -} - -=head2 ForwardTransaction TRANSACTION, To => '', Cc => '', Bcc => '' - -Forwards transaction with all attachments as 'message/rfc822'. - -=cut - -sub ForwardTransaction { - my $txn = shift; - my %args = ( To => '', Cc => '', Bcc => '', @_ ); - - my $entity = $txn->ContentAsMIME; - - 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 => '' - -Forwards a ticket's Create and Correspond Transactions and their Attachments as 'message/rfc822'. - -=cut - -sub ForwardTicket { - my $ticket = shift; - my %args = ( To => '', Cc => '', Bcc => '', @_ ); - - my $txns = $ticket->Transactions; - $txns->Limit( - FIELD => 'Type', - VALUE => $_, - ) for qw(Create Correspond); - - my $entity = MIME::Entity->build( - Type => 'multipart/mixed', - Description => 'forwarded ticket', - ); - $entity->add_part( $_ ) foreach - map $_->ContentAsMIME, - @{ $txns->ItemsArrayRef }; - - 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 => '' - -Forwards an Entity representing Ticket or Transaction as 'message/rfc822'. Entity is wrapped into Template. - -=cut - -sub SendForward { - my (%args) = ( - Entity => undef, - Ticket => undef, - Transaction => undef, - Template => 'Forward', - To => '', Cc => '', Bcc => '', - @_ - ); - - my $txn = $args{'Transaction'}; - my $ticket = $args{'Ticket'}; - $ticket ||= $txn->Object if $txn; - - my $entity = $args{'Entity'}; - unless ( $entity ) { - require Carp; - $RT::Logger->error(Carp::longmess("No entity provided")); - return (0, $ticket->loc("Couldn't send email")); - } - - my ($template, $msg) = PrepareEmailUsingTemplate( - Template => $args{'Template'}, - Arguments => { - Ticket => $ticket, - Transaction => $txn, - }, - ); - - my $mail; - if ( $template ) { - $mail = $template->MIMEObj; - } else { - $RT::Logger->warning($msg); - } - unless ( $mail ) { - $RT::Logger->warning("Couldn't generate email using template '$args{Template}'"); - - my $description; - unless ( $args{'Transaction'} ) { - $description = 'This is forward of ticket #'. $ticket->id; - } else { - $description = 'This is forward of transaction #' - . $txn->id ." of a ticket #". $txn->ObjectId; - } - $mail = MIME::Entity->build( - Type => 'text/plain', - Data => $description, - ); - } - - $mail->head->set( $_ => EncodeToMIME( String => $args{$_} ) ) - foreach grep defined $args{$_}, qw(To Cc Bcc); - - $mail->make_multipart unless $mail->is_multipart; - $mail->add_part( $entity ); - - my $from; - unless (defined $mail->head->get('Subject')) { - my $subject = ''; - $subject = $txn->Subject if $txn; - $subject ||= $ticket->Subject if $ticket; - - unless ( RT->Config->Get('ForwardFromUser') ) { - # XXX: what if want to forward txn of other object than ticket? - $subject = AddSubjectTag( $subject, $ticket ); - } - - $mail->head->set( Subject => EncodeToMIME( String => "Fwd: $subject" ) ); - } - - $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("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->EmailAddress; - } - else { - return $ticket->QueueObj->CorrespondAddress - || RT->Config->Get('CorrespondAddress'); - } -} - -=head2 SignEncrypt Entity => undef, Sign => 0, Encrypt => 0 - -Signs and encrypts message using L, but as well -handle errors with users' keys. - -If a recipient has no key or has other problems with it, then the -unction sends a error to him using 'Error: public key' template. -Also, notifies RT's owner using template 'Error to RT owner: public key' -to inform that there are problems with users' keys. Then we filter -all bad recipients and retry. - -Returns 1 on success, 0 on error and -1 if all recipients are bad and -had been filtered out. - -=cut - -sub SignEncrypt { - my %args = ( - Entity => undef, - Sign => 0, - Encrypt => 0, - @_ - ); - return 1 unless $args{'Sign'} || $args{'Encrypt'}; - - my $msgid = $args{'Entity'}->head->get('Message-ID') || ''; - chomp $msgid; - - $RT::Logger->debug("$msgid Signing message") if $args{'Sign'}; - $RT::Logger->debug("$msgid Encrypting message") if $args{'Encrypt'}; - - require RT::Crypt::GnuPG; - my %res = RT::Crypt::GnuPG::SignEncrypt( %args ); - return 1 unless $res{'exit_code'}; - - my @status = RT::Crypt::GnuPG::ParseStatus( $res{'status'} ); - - my @bad_recipients; - foreach my $line ( @status ) { - # if the passphrase fails, either you have a bad passphrase - # or gpg-agent has died. That should get caught in Create and - # Update, but at least throw an error here - if (($line->{'Operation'}||'') eq 'PassphraseCheck' - && $line->{'Status'} =~ /^(?:BAD|MISSING)$/ ) { - $RT::Logger->error( "$line->{'Status'} PASSPHRASE: $line->{'Message'}" ); - return 0; - } - next unless ($line->{'Operation'}||'') eq 'RecipientsCheck'; - next if $line->{'Status'} eq 'DONE'; - $RT::Logger->error( $line->{'Message'} ); - push @bad_recipients, $line; - } - return 0 unless @bad_recipients; - - $_->{'AddressObj'} = (Email::Address->parse( $_->{'Recipient'} ))[0] - foreach @bad_recipients; - - foreach my $recipient ( @bad_recipients ) { - my $status = SendEmailUsingTemplate( - To => $recipient->{'AddressObj'}->address, - Template => 'Error: public key', - Arguments => { - %$recipient, - TicketObj => $args{'Ticket'}, - TransactionObj => $args{'Transaction'}, - }, - ); - unless ( $status ) { - $RT::Logger->error("Couldn't send 'Error: public key'"); - } - } - - my $status = SendEmailUsingTemplate( - To => RT->Config->Get('OwnerEmail'), - Template => 'Error to RT owner: public key', - Arguments => { - BadRecipients => \@bad_recipients, - TicketObj => $args{'Ticket'}, - TransactionObj => $args{'Transaction'}, - }, - ); - unless ( $status ) { - $RT::Logger->error("Couldn't send 'Error to RT owner: public key'"); - } - - DeleteRecipientsFromHead( - $args{'Entity'}->head, - map $_->{'AddressObj'}->address, @bad_recipients - ); - - unless ( $args{'Entity'}->head->get('To') - || $args{'Entity'}->head->get('Cc') - || $args{'Entity'}->head->get('Bcc') ) - { - $RT::Logger->debug("$msgid No recipients that have public key, not sending"); - return -1; - } - - # redo without broken recipients - %res = RT::Crypt::GnuPG::SignEncrypt( %args ); - return 0 if $res{'exit_code'}; - - return 1; -} - -use MIME::Words (); - -=head2 EncodeToMIME - -Takes a hash with a String and a Charset. Returns the string encoded -according to RFC2047, using B (base64 based) encoding. - -String must be a perl string, octets are returned. - -If Charset is not provided then $EmailOutputEncoding config option -is used, or "latin-1" if that is not set. - -=cut - -sub EncodeToMIME { - my %args = ( - String => undef, - Charset => undef, - @_ - ); - my $value = $args{'String'}; - return $value unless $value; # 0 is perfect ascii - my $charset = $args{'Charset'} || RT->Config->Get('EmailOutputEncoding'); - my $encoding = 'B'; - - # using RFC2047 notation, sec 2. - # encoded-word = "=?" charset "?" encoding "?" encoded-text "?=" - - # An 'encoded-word' may not be more than 75 characters long - # - # MIME encoding increases 4/3*(number of bytes), and always in multiples - # of 4. Thus we have to find the best available value of bytes available - # for each chunk. - # - # First we get the integer max which max*4/3 would fit on space. - # Then we find the greater multiple of 3 lower or equal than $max. - my $max = int( - ( ( 75 - length( '=?' . $charset . '?' . $encoding . '?' . '?=' ) ) - * 3 - ) / 4 - ); - $max = int( $max / 3 ) * 3; - - chomp $value; - - if ( $max <= 0 ) { - - # gives an error... - $RT::Logger->crit("Can't encode! Charset or encoding too big."); - return ($value); - } - - return ($value) if $value =~ /^(?:[\t\x20-\x7e]|\x0D*\x0A[ \t])+$/s; - - $value =~ s/\s+$//; - - # we need perl string to split thing char by char - Encode::_utf8_on($value) unless Encode::is_utf8($value); - - my ( $tmp, @chunks ) = ( '', () ); - while ( length $value ) { - my $char = substr( $value, 0, 1, '' ); - my $octets = Encode::encode( $charset, $char ); - if ( length($tmp) + length($octets) > $max ) { - push @chunks, $tmp; - $tmp = ''; - } - $tmp .= $octets; - } - push @chunks, $tmp if length $tmp; - - # encode an join chuncks - $value = join "\n ", - map MIME::Words::encode_mimeword( $_, $encoding, $charset ), - @chunks; - return ($value); -} - -sub CreateUser { - my ( $Username, $Address, $Name, $ErrorsTo, $entity ) = @_; - - my $NewUser = RT::User->new( RT->SystemUser ); - - my ( $Val, $Message ) = $NewUser->Create( - Name => ( $Username || $Address ), - EmailAddress => $Address, - RealName => $Name, - Password => undef, - Privileged => 0, - Comments => 'Autocreated on ticket submission', - ); - - unless ($Val) { - - # Deal with the race condition of two account creations at once - if ($Username) { - $NewUser->LoadByName($Username); - } - - unless ( $NewUser->Id ) { - $NewUser->LoadByEmail($Address); - } - - unless ( $NewUser->Id ) { - MailError( - To => $ErrorsTo, - Subject => "User could not be created", - Explanation => - "User creation failed in mailgateway: $Message", - MIMEObj => $entity, - LogLevel => 'crit', - ); - } - } - - #Load the new user object - my $CurrentUser = RT::CurrentUser->new; - $CurrentUser->LoadByEmail( $Address ); - - unless ( $CurrentUser->id ) { - $RT::Logger->warning( - "Couldn't load user '$Address'." . "giving up" ); - MailError( - To => $ErrorsTo, - Subject => "User could not be loaded", - Explanation => - "User '$Address' could not be loaded in the mail gateway", - MIMEObj => $entity, - LogLevel => 'crit' - ); - } - - return $CurrentUser; -} - - - -=head2 ParseCcAddressesFromHead HASH - -Takes a hash containing QueueObj, Head and CurrentUser objects. -Returns a list of all email addresses in the To and Cc -headers b the current Queue's email addresses, the CurrentUser's -email address and anything that the configuration sub RT::IsRTAddress matches. - -=cut - -sub ParseCcAddressesFromHead { - my %args = ( - Head => undef, - QueueObj => undef, - CurrentUser => undef, - @_ - ); - - my $current_address = lc $args{'CurrentUser'}->EmailAddress; - my $user = $args{'CurrentUser'}->UserObj; - - return - grep { $_ ne $current_address - && !RT::EmailParser->IsRTAddress( $_ ) - && !IgnoreCcAddress( $_ ) - } - map lc $user->CanonicalizeEmailAddress( $_->address ), - map RT::EmailParser->CleanupAddresses( Email::Address->parse( $args{'Head'}->get( $_ ) ) ), - qw(To Cc); -} - -=head2 IgnoreCcAddress ADDRESS - -Returns true if ADDRESS matches the $IgnoreCcRegexp config variable. - -=cut - -sub IgnoreCcAddress { - my $address = shift; - if ( my $address_re = RT->Config->Get('IgnoreCcRegexp') ) { - return 1 if $address =~ /$address_re/i; - } - return undef; -} - -=head2 ParseSenderAddressFromHead HEAD - -Takes a MIME::Header object. Returns (user@host, friendly name, errors) -where the first two values are the From (evaluated in order of -Reply-To:, From:, Sender). - -A list of error messages may be returned even when a Sender value is -found, since it could be a parse error for another (checked earlier) -sender field. In this case, the errors aren't fatal, but may be useful -to investigate the parse failure. - -=cut - -sub ParseSenderAddressFromHead { - my $head = shift; - my @sender_headers = ('Reply-To', 'From', 'Sender'); - my @errors; # Accumulate any errors - - #Figure out who's sending this message. - foreach my $header ( @sender_headers ) { - my $addr_line = $head->get($header) || next; - my ($addr, $name) = ParseAddressFromHeader( $addr_line ); - # only return if the address is not empty - return ($addr, $name, @errors) if $addr; - - chomp $addr_line; - push @errors, "$header: $addr_line"; - } - - return (undef, undef, @errors); -} - -=head2 ParseErrorsToAddressFromHead HEAD - -Takes a MIME::Header object. Return a single value : user@host -of the From (evaluated in order of Return-path:,Errors-To:,Reply-To:, -From:, Sender) - -=cut - -sub ParseErrorsToAddressFromHead { - my $head = shift; - - #Figure out who's sending this message. - - foreach my $header ( 'Errors-To', 'Reply-To', 'From', 'Sender' ) { - - # If there's a header of that name - my $headerobj = $head->get($header); - if ($headerobj) { - my ( $addr, $name ) = ParseAddressFromHeader($headerobj); - - # If it's got actual useful content... - return ($addr) if ($addr); - } - } -} - - - -=head2 ParseAddressFromHeader ADDRESS - -Takes an address from C<$head->get('Line')> and returns a tuple: user@host, friendly name - -=cut - -sub ParseAddressFromHeader { - my $Addr = shift; - - # Some broken mailers send: ""Vincent, Jesse"" . Hate - $Addr =~ s/\"\"(.*?)\"\"/\"$1\"/g; - my @Addresses = RT::EmailParser->ParseEmailAddress($Addr); - - my ($AddrObj) = grep ref $_, @Addresses; - unless ( $AddrObj ) { - return ( undef, undef ); - } - - return ( $AddrObj->address, $AddrObj->phrase ); -} - -=head2 DeleteRecipientsFromHead HEAD RECIPIENTS - -Gets a head object and list of addresses. -Deletes addresses from To, Cc or Bcc fields. - -=cut - -sub DeleteRecipientsFromHead { - my $head = shift; - my %skip = map { lc $_ => 1 } @_; - - foreach my $field ( qw(To Cc Bcc) ) { - $head->set( $field => - join ', ', map $_->format, grep !$skip{ lc $_->address }, - Email::Address->parse( $head->get( $field ) ) - ); - } -} - -sub GenMessageId { - my %args = ( - Ticket => undef, - Scrip => undef, - ScripAction => undef, - @_ - ); - my $org = RT->Config->Get('Organization'); - my $ticket_id = ( ref $args{'Ticket'}? $args{'Ticket'}->id : $args{'Ticket'} ) || 0; - my $scrip_id = ( ref $args{'Scrip'}? $args{'Scrip'}->id : $args{'Scrip'} ) || 0; - my $sent = ( ref $args{'ScripAction'}? $args{'ScripAction'}->{'_Message_ID'} : 0 ) || 0; - - return "" ; -} - -sub SetInReplyTo { - my %args = ( - Message => undef, - InReplyTo => undef, - Ticket => undef, - @_ - ); - return unless $args{'Message'} && $args{'InReplyTo'}; - - my $get_header = sub { - my @res; - if ( $args{'InReplyTo'}->isa('MIME::Entity') ) { - @res = $args{'InReplyTo'}->head->get( shift ); - } else { - @res = $args{'InReplyTo'}->GetHeader( shift ) || ''; - } - return grep length, map { split /\s+/m, $_ } grep defined, @res; - }; - - my @id = $get_header->('Message-ID'); - #XXX: custom header should begin with X- otherwise is violation of the standard - my @rtid = $get_header->('RT-Message-ID'); - my @references = $get_header->('References'); - unless ( @references ) { - @references = $get_header->('In-Reply-To'); - } - push @references, @id, @rtid; - if ( $args{'Ticket'} ) { - my $pseudo_ref = 'id .'@'. RT->Config->Get('Organization') .'>'; - push @references, $pseudo_ref unless grep $_ eq $pseudo_ref, @references; - } - @references = splice @references, 4, -6 - if @references > 10; - - my $mail = $args{'Message'}; - $mail->head->set( 'In-Reply-To' => Encode::encode_utf8(join ' ', @rtid? (@rtid) : (@id)) ) if @id || @rtid; - $mail->head->set( 'References' => Encode::encode_utf8(join ' ', @references) ); -} - -sub ExtractTicketId { - my $entity = shift; - - my $subject = $entity->head->get('Subject') || ''; - chomp $subject; - return ParseTicketId( $subject ); -} - -sub ParseTicketId { - my $Subject = shift; - - my $rtname = RT->Config->Get('rtname'); - my $test_name = RT->Config->Get('EmailSubjectTagRegex') || qr/\Q$rtname\E/i; - - my $id; - if ( $Subject =~ s/\[$test_name\s+\#(\d+)\s*\]//i ) { - $id = $1; - } else { - foreach my $tag ( RT->System->SubjectTag ) { - next unless $Subject =~ s/\[\Q$tag\E\s+\#(\d+)\s*\]//i; - $id = $1; - last; - } - } - return undef unless $id; - - $RT::Logger->debug("Found a ticket ID. It's $id"); - return $id; -} - -sub AddSubjectTag { - my $subject = shift; - my $ticket = shift; - unless ( ref $ticket ) { - my $tmp = RT::Ticket->new( RT->SystemUser ); - $tmp->Load( $ticket ); - $ticket = $tmp; - } - my $id = $ticket->id; - my $queue_tag = $ticket->QueueObj->SubjectTag; - - my $tag_re = RT->Config->Get('EmailSubjectTagRegex'); - unless ( $tag_re ) { - my $tag = $queue_tag || RT->Config->Get('rtname'); - $tag_re = qr/\Q$tag\E/; - } elsif ( $queue_tag ) { - $tag_re = qr/$tag_re|\Q$queue_tag\E/; - } - return $subject if $subject =~ /\[$tag_re\s+#$id\]/; - - $subject =~ s/(\r\n|\n|\s)/ /g; - chomp $subject; - return "[". ($queue_tag || RT->Config->Get('rtname')) ." #$id] $subject"; -} - - -=head2 Gateway ARGSREF - - -Takes parameters: - - action - queue - message - - -This performs all the "guts" of the mail rt-mailgate program, and is -designed to be called from the web interface with a message, user -object, and so on. - -Can also take an optional 'ticket' parameter; this ticket id overrides -any ticket id found in the subject. - -Returns: - - An array of: - - (status code, message, optional ticket object) - - status code is a numeric value. - - for temporary failures, the status code should be -75 - - for permanent failures which are handled by RT, the status code - should be 0 - - for succces, the status code should be 1 - - - -=cut - -sub _LoadPlugins { - my @mail_plugins = @_; - - my @res; - foreach my $plugin (@mail_plugins) { - if ( ref($plugin) eq "CODE" ) { - push @res, $plugin; - } elsif ( !ref $plugin ) { - my $Class = $plugin; - $Class = "RT::Interface::Email::" . $Class - unless $Class =~ /^RT::/; - $Class->require or - do { $RT::Logger->error("Couldn't load $Class: $@"); next }; - - no strict 'refs'; - unless ( defined *{ $Class . "::GetCurrentUser" }{CODE} ) { - $RT::Logger->crit( "No GetCurrentUser code found in $Class module"); - next; - } - push @res, $Class; - } else { - $RT::Logger->crit( "$plugin - is not class name or code reference"); - } - } - return @res; -} - -sub Gateway { - my $argsref = shift; - my %args = ( - action => 'correspond', - queue => '1', - ticket => undef, - message => undef, - %$argsref - ); - - my $SystemTicket; - my $Right; - - # Validate the action - my ( $status, @actions ) = IsCorrectAction( $args{'action'} ); - unless ($status) { - return ( - -75, - "Invalid 'action' parameter " - . $actions[0] - . " for queue " - . $args{'queue'}, - undef - ); - } - - my $parser = RT::EmailParser->new(); - $parser->SmartParseMIMEEntityFromScalar( - Message => $args{'message'}, - Decode => 0, - Exact => 1, - ); - - my $Message = $parser->Entity(); - unless ($Message) { - MailError( - Subject => "RT Bounce: Unparseable message", - Explanation => "RT couldn't process the message below", - Attach => $args{'message'} - ); - - return ( 0, - "Failed to parse this message. Something is likely badly wrong with the message" - ); - } - - my @mail_plugins = grep $_, RT->Config->Get('MailPlugins'); - push @mail_plugins, "Auth::MailFrom" unless @mail_plugins; - @mail_plugins = _LoadPlugins( @mail_plugins ); - - my %skip_plugin; - foreach my $class( grep !ref, @mail_plugins ) { - # check if we should apply filter before decoding - my $check_cb = do { - no strict 'refs'; - *{ $class . "::ApplyBeforeDecode" }{CODE}; - }; - next unless defined $check_cb; - next unless $check_cb->( - Message => $Message, - RawMessageRef => \$args{'message'}, - ); - - $skip_plugin{ $class }++; - - my $Code = do { - no strict 'refs'; - *{ $class . "::GetCurrentUser" }{CODE}; - }; - my ($status, $msg) = $Code->( - Message => $Message, - RawMessageRef => \$args{'message'}, - ); - next if $status > 0; - - if ( $status == -2 ) { - return (1, $msg, undef); - } elsif ( $status == -1 ) { - return (0, $msg, undef); - } - } - @mail_plugins = grep !$skip_plugin{"$_"}, @mail_plugins; - $parser->_DecodeBodies; - $parser->RescueOutlook; - $parser->_PostProcessNewEntity; - - my $head = $Message->head; - my $ErrorsTo = ParseErrorsToAddressFromHead( $head ); - my $Sender = (ParseSenderAddressFromHead( $head ))[0]; - my $From = $head->get("From"); - chomp $From if defined $From; - - my $MessageId = $head->get('Message-ID') - || "Config->Get('Organization') .'>'; - - #Pull apart the subject line - my $Subject = $head->get('Subject') || ''; - chomp $Subject; - - # 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( - Message => $Message, - ErrorsTo => $ErrorsTo, - Subject => $Subject, - MessageId => $MessageId - ); - - # Do not pass loop messages to MailPlugins, to make sure the loop - # is broken, unless $RT::StoreLoops is set. - if ($IsALoop && !$should_store_machine_generated_message) { - return ( 0, $result, undef ); - } - # }}} - - $args{'ticket'} ||= ExtractTicketId( $Message ); - - # ExtractTicketId may have been overridden, and edited the Subject - my $NewSubject = $Message->head->get('Subject'); - chomp $NewSubject; - - $SystemTicket = RT::Ticket->new( RT->SystemUser ); - $SystemTicket->Load( $args{'ticket'} ) if ( $args{'ticket'} ) ; - if ( $SystemTicket->id ) { - $Right = 'ReplyToTicket'; - } else { - $Right = 'CreateTicket'; - } - - #Set up a queue object - my $SystemQueueObj = RT::Queue->new( RT->SystemUser ); - $SystemQueueObj->Load( $args{'queue'} ); - - # We can safely have no queue of we have a known-good ticket - unless ( $SystemTicket->id || $SystemQueueObj->id ) { - return ( -75, "RT couldn't find the queue: " . $args{'queue'}, undef ); - } - - my ($AuthStat, $CurrentUser, $error) = GetAuthenticationLevel( - MailPlugins => \@mail_plugins, - Actions => \@actions, - Message => $Message, - RawMessageRef => \$args{message}, - SystemTicket => $SystemTicket, - SystemQueue => $SystemQueueObj, - ); - - # 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. - unless ( $AuthStat == -1 ) { - _NoAuthorizedUserFound( - Right => $Right, - Message => $Message, - Requestor => $ErrorsTo, - Queue => $args{'queue'} - ); - - } - return ( 0, "Could not load a valid user", undef ); - } - - # If we got a user, but they don't have the right to say things - if ( $AuthStat == 0 ) { - MailError( - To => $ErrorsTo, - Subject => "Permission Denied", - Explanation => - "You do not have permission to communicate with RT", - MIMEObj => $Message - ); - return ( - 0, - ($CurrentUser->EmailAddress || $CurrentUser->Name) - . " ($Sender) tried to submit a message to " - . $args{'Queue'} - . " without permission.", - undef - ); - } - - - unless ($should_store_machine_generated_message) { - return ( 0, $result, undef ); - } - - # if plugin's updated SystemTicket then update arguments - $args{'ticket'} = $SystemTicket->Id if $SystemTicket && $SystemTicket->Id; - - my $Ticket = RT::Ticket->new($CurrentUser); - - if ( !$args{'ticket'} && grep /^(comment|correspond)$/, @actions ) - { - - my @Cc; - my @Requestors = ( $CurrentUser->id ); - - if (RT->Config->Get('ParseNewMessageForTicketCcs')) { - @Cc = ParseCcAddressesFromHead( - Head => $head, - CurrentUser => $CurrentUser, - QueueObj => $SystemQueueObj - ); - } - - $head->replace('X-RT-Interface' => 'Email'); - - my ( $id, $Transaction, $ErrStr ) = $Ticket->Create( - Queue => $SystemQueueObj->Id, - Subject => $NewSubject, - Requestor => \@Requestors, - Cc => \@Cc, - MIMEObj => $Message - ); - if ( $id == 0 ) { - MailError( - To => $ErrorsTo, - Subject => "Ticket creation failed: $Subject", - Explanation => $ErrStr, - MIMEObj => $Message - ); - return ( 0, "Ticket creation From: $From failed: $ErrStr", $Ticket ); - } - - # strip comments&corresponds from the actions we don't need - # to record them if we've created the ticket just now - @actions = grep !/^(comment|correspond)$/, @actions; - $args{'ticket'} = $id; - - } elsif ( $args{'ticket'} ) { - - $Ticket->Load( $args{'ticket'} ); - unless ( $Ticket->Id ) { - my $error = "Could not find a ticket with id " . $args{'ticket'}; - MailError( - To => $ErrorsTo, - Subject => "Message not recorded: $Subject", - Explanation => $error, - MIMEObj => $Message - ); - - return ( 0, $error ); - } - $args{'ticket'} = $Ticket->id; - } else { - return ( 1, "Success", $Ticket ); - } - - # }}} - - my $unsafe_actions = RT->Config->Get('UnsafeEmailCommands'); - foreach my $action (@actions) { - - # If the action is comment, add a comment. - if ( $action =~ /^(?:comment|correspond)$/i ) { - my $method = ucfirst lc $action; - my ( $status, $msg ) = $Ticket->$method( MIMEObj => $Message ); - unless ($status) { - - #Warn the sender that we couldn't actually submit the comment. - MailError( - To => $ErrorsTo, - Subject => "Message not recorded ($method): $Subject", - Explanation => $msg, - MIMEObj => $Message - ); - return ( 0, "Message From: $From not recorded: $msg", $Ticket ); - } - } elsif ($unsafe_actions) { - my ( $status, $msg ) = _RunUnsafeAction( - Action => $action, - ErrorsTo => $ErrorsTo, - Message => $Message, - Ticket => $Ticket, - CurrentUser => $CurrentUser, - ); - return ($status, $msg, $Ticket) unless $status == 1; - } - } - return ( 1, "Success", $Ticket ); -} - -=head2 GetAuthenticationLevel - - # Authentication Level - # -1 - Get out. this user has been explicitly declined - # 0 - User may not do anything (Not used at the moment) - # 1 - Normal user - # 2 - User is allowed to specify status updates etc. a la enhanced-mailgate - -=cut - -sub GetAuthenticationLevel { - my %args = ( - MailPlugins => [], - Actions => [], - Message => undef, - RawMessageRef => undef, - SystemTicket => undef, - SystemQueue => undef, - @_, - ); - - my ( $CurrentUser, $AuthStat, $error ); - - # Initalize AuthStat so comparisons work correctly - $AuthStat = -9999999; - - # if plugin returns AuthStat -2 we skip action - # NOTE: this is experimental API and it would be changed - my %skip_action = (); - - # Since this needs loading, no matter what - foreach (@{ $args{MailPlugins} }) { - my ($Code, $NewAuthStat); - if ( ref($_) eq "CODE" ) { - $Code = $_; - } else { - no strict 'refs'; - $Code = *{ $_ . "::GetCurrentUser" }{CODE}; - } - - foreach my $action (@{ $args{Actions} }) { - ( $CurrentUser, $NewAuthStat ) = $Code->( - Message => $args{Message}, - RawMessageRef => $args{RawMessageRef}, - CurrentUser => $CurrentUser, - AuthLevel => $AuthStat, - Action => $action, - Ticket => $args{SystemTicket}, - Queue => $args{SystemQueue}, - ); - -# You get the highest level of authentication you were assigned, unless you get the magic -1 -# If a module returns a "-1" then we discard the ticket, so. - $AuthStat = $NewAuthStat - if ( $NewAuthStat > $AuthStat or $NewAuthStat == -1 or $NewAuthStat == -2 ); - - last if $AuthStat == -1; - $skip_action{$action}++ if $AuthStat == -2; - } - - # strip actions we should skip - @{$args{Actions}} = grep !$skip_action{$_}, @{$args{Actions}} - if $AuthStat == -2; - last unless @{$args{Actions}}; - - last if $AuthStat == -1; - } - - return $AuthStat if !wantarray; - - return ($AuthStat, $CurrentUser, $error); -} - -sub _RunUnsafeAction { - my %args = ( - Action => undef, - ErrorsTo => undef, - Message => undef, - Ticket => undef, - CurrentUser => undef, - @_ - ); - - my $From = $args{Message}->head->get("From"); - - if ( $args{'Action'} =~ /^take$/i ) { - my ( $status, $msg ) = $args{'Ticket'}->SetOwner( $args{'CurrentUser'}->id ); - unless ($status) { - MailError( - To => $args{'ErrorsTo'}, - Subject => "Ticket not taken", - Explanation => $msg, - MIMEObj => $args{'Message'} - ); - return ( 0, "Ticket not taken, by email From: $From" ); - } - } elsif ( $args{'Action'} =~ /^resolve$/i ) { - my $new_status = $args{'Ticket'}->FirstInactiveStatus; - if ($new_status) { - my ( $status, $msg ) = $args{'Ticket'}->SetStatus($new_status); - unless ($status) { - - #Warn the sender that we couldn't actually submit the comment. - MailError( - To => $args{'ErrorsTo'}, - Subject => "Ticket not resolved", - Explanation => $msg, - MIMEObj => $args{'Message'} - ); - return ( 0, "Ticket not resolved, by email From: $From" ); - } - } - } else { - return ( 0, "Not supported unsafe action $args{'Action'}, by email From: $From", $args{'Ticket'} ); - } - return ( 1, "Success" ); -} - -=head2 _NoAuthorizedUserFound - -Emails the RT Owner and the requestor when the auth plugins return "No auth user found" - -=cut - -sub _NoAuthorizedUserFound { - my %args = ( - Right => undef, - Message => undef, - Requestor => undef, - Queue => undef, - @_ - ); - - # Notify the RT Admin of the failure. - MailError( - To => RT->Config->Get('OwnerEmail'), - Subject => "Could not load a valid user", - Explanation => < $args{'Message'}, - LogLevel => 'error' - ); - - # Also notify the requestor that his request has been dropped. - if ($args{'Requestor'} ne RT->Config->Get('OwnerEmail')) { - MailError( - To => $args{'Requestor'}, - Subject => "Could not load a valid user", - Explanation => < $args{'Message'}, - LogLevel => 'error' - ); - } -} - -=head2 _HandleMachineGeneratedMail - -Takes named params: - Message - ErrorsTo - Subject - -Checks the message to see if it's a bounce, if it looks like a loop, if it's autogenerated, etc. -Returns a triple of ("Should we continue (boolean)", "New value for $ErrorsTo", "Status message", -"This message appears to be a loop (boolean)" ); - -=cut - -sub _HandleMachineGeneratedMail { - my %args = ( Message => undef, ErrorsTo => undef, Subject => undef, MessageId => undef, @_ ); - my $head = $args{'Message'}->head; - my $ErrorsTo = $args{'ErrorsTo'}; - - my $IsBounce = CheckForBounce($head); - - my $IsAutoGenerated = CheckForAutoGenerated($head); - - my $IsSuspiciousSender = CheckForSuspiciousSender($head); - - my $IsALoop = CheckForLoops($head); - - my $SquelchReplies = 0; - - my $owner_mail = RT->Config->Get('OwnerEmail'); - - #If the message is autogenerated, we need to know, so we can not - # send mail to the sender - if ( $IsBounce || $IsSuspiciousSender || $IsAutoGenerated || $IsALoop ) { - $SquelchReplies = 1; - $ErrorsTo = $owner_mail; - } - - # Warn someone if it's a loop, before we drop it on the ground - if ($IsALoop) { - $RT::Logger->crit("RT Received mail (".$args{MessageId}.") from itself."); - - #Should we mail it to RTOwner? - if ( RT->Config->Get('LoopsToRTOwner') ) { - MailError( - To => $owner_mail, - Subject => "RT Bounce: ".$args{'Subject'}, - Explanation => "RT thinks this message may be a bounce", - MIMEObj => $args{Message} - ); - } - - #Do we actually want to store it? - return ( 0, $ErrorsTo, "Message Bounced", $IsALoop ) - unless RT->Config->Get('StoreLoops'); - } - - # Squelch replies if necessary - # Don't let the user stuff the RT-Squelch-Replies-To header. - if ( $head->get('RT-Squelch-Replies-To') ) { - $head->replace( - 'RT-Relocated-Squelch-Replies-To', - $head->get('RT-Squelch-Replies-To') - ); - $head->delete('RT-Squelch-Replies-To'); - } - - if ($SquelchReplies) { - - # Squelch replies to the sender, and also leave a clue to - # allow us to squelch ALL outbound messages. This way we - # can punt the logic of "what to do when we get a bounce" - # 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->replace( 'RT-Squelch-Replies-To', $Sender ); - $head->replace( 'RT-DetectedAutoGenerated', 'true' ); - } - return ( 1, $ErrorsTo, "Handled machine detection", $IsALoop ); -} - -=head2 IsCorrectAction - -Returns a list of valid actions we've found for this message - -=cut - -sub IsCorrectAction { - my $action = shift; - my @actions = grep $_, split /-/, $action; - return ( 0, '(no value)' ) unless @actions; - foreach ( @actions ) { - return ( 0, $_ ) unless /^(?:comment|correspond|take|resolve)$/; - } - return ( 1, @actions ); -} - -sub _RecordSendEmailFailure { - my $ticket = shift; - if ($ticket) { - $ticket->_RecordNote( - NoteType => 'SystemError', - Content => "Sending the previous mail has failed. Please contact your admin, they can find more details in the logs.", - ); - return 1; - } - else { - $RT::Logger->error( "Can't record send email failure as ticket is missing" ); - return; - } -} - -RT::Base->_ImportOverlays(); - -1; diff --git a/rt/lib/RT/Interface/Web.pm.orig b/rt/lib/RT/Interface/Web.pm.orig deleted file mode 100644 index 59d315431..000000000 --- a/rt/lib/RT/Interface/Web.pm.orig +++ /dev/null @@ -1,3454 +0,0 @@ -# BEGIN BPS TAGGED BLOCK {{{ -# -# COPYRIGHT: -# -# This software is Copyright (c) 1996-2014 Best Practical Solutions, LLC -# -# -# (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 }}} - -## Portions Copyright 2000 Tobias Brox - -## This is a library of static subs to be used by the Mason web -## interface to RT - -=head1 NAME - -RT::Interface::Web - - -=cut - -use strict; -use warnings; - -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(); - -=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 - -does a css-busting but minimalist escaping of whatever html you're passing in. - -=cut - -sub EscapeUTF8 { - my $ref = shift; - return unless defined $$ref; - - $$ref =~ s/&/&/g; - $$ref =~ s//>/g; - $$ref =~ s/\(/(/g; - $$ref =~ s/\)/)/g; - $$ref =~ s/"/"/g; - $$ref =~ s/'/'/g; -} - - - -=head2 EscapeURI SCALARREF - -Escapes URI component according to RFC2396 - -=cut - -sub EscapeURI { - my $ref = shift; - return unless defined $$ref; - - use bytes; - $$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. - -=cut - -sub EncodeJSON { - JSON::to_json(shift, { utf8 => 1, allow_nonref => 1 }); -} - -sub _encode_surrogates { - my $uni = $_[0] - 0x10000; - return ($uni / 0x400 + 0xD800, $uni % 0x400 + 0xDC00); -} - -sub EscapeJS { - my $ref = shift; - return unless defined $$ref; - - $$ref = "'" . join('', - map { - chr($_) =~ /[a-zA-Z0-9]/ ? chr($_) : - $_ <= 255 ? sprintf("\\x%02X", $_) : - $_ <= 65535 ? sprintf("\\u%04X", $_) : - sprintf("\\u%X\\u%X", _encode_surrogates($_)) - } unpack('U*', $$ref)) - . "'"; -} - -=head2 WebCanonicalizeInfo(); - -Different web servers set different environmental varibles. This -function must return something suitable for REMOTE_USER. By default, -just downcase $ENV{'REMOTE_USER'} - -=cut - -sub WebCanonicalizeInfo { - return $ENV{'REMOTE_USER'} ? lc $ENV{'REMOTE_USER'} : $ENV{'REMOTE_USER'}; -} - - - -=head2 WebExternalAutoInfo($user); - -Returns a hash of user attributes, used when WebExternalAuto is set. - -=cut - -sub WebExternalAutoInfo { - my $user = shift; - - my %user_info; - - # default to making Privileged users, even if they specify - # some other default Attributes - if ( !$RT::AutoCreate - || ( ref($RT::AutoCreate) && not exists $RT::AutoCreate->{Privileged} ) ) - { - $user_info{'Privileged'} = 1; - } - - if ( $^O !~ /^(?:riscos|MacOS|MSWin32|dos|os2)$/ ) { - - # Populate fields with information from Unix /etc/passwd - - my ( $comments, $realname ) = ( getpwnam($user) )[ 5, 6 ]; - $user_info{'Comments'} = $comments if defined $comments; - $user_info{'RealName'} = $realname if defined $realname; - } elsif ( $^O eq 'MSWin32' and eval 'use Net::AdminMisc; 1' ) { - - # Populate fields with information from NT domain controller - } - - # and return the wad of stuff - 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 and $RT::Handle->TransactionDepth; - - MaybeEnableSQLStatementLog(); - - # avoid reentrancy, as suggested by masonbook - local *HTML::Mason::Commands::session unless $HTML::Mason::Commands::m->is_subrequest; - - $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); - local $HTML::Mason::Commands::DECODED_ARGS = $ARGS; - PreprocessTimeUpdates($ARGS); - - InitializeMenu(); - MaybeShowInstallModePage(); - - $HTML::Mason::Commands::m->comp( '/Elements/SetupSessionCookie', %$ARGS ); - SendSessionCookie(); - - if ( _UserLoggedIn() ) { - # make user info up to date - $HTML::Mason::Commands::session{'CurrentUser'} - ->Load( $HTML::Mason::Commands::session{'CurrentUser'}->id ); - undef $HTML::Mason::Commands::session{'CurrentUser'}->{'LangHandle'}; - } - else { - $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new(); - } - - # Process session-related callbacks before any auth attempts - $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Session', CallbackPage => '/autohandler' ); - - MaybeRejectPrivateComponentRequest(); - - MaybeShowNoAuthPage($ARGS); - - AttemptExternalAuth($ARGS) if RT->Config->Get('WebExternalAuthContinuous') or not _UserLoggedIn(); - - _ForceLogout() unless _UserLoggedIn(); - - # Process per-page authentication callbacks - $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Auth', CallbackPage => '/autohandler' ); - - if ( $ARGS->{'NotMobile'} ) { - $HTML::Mason::Commands::session{'NotMobile'} = 1; - } - - unless ( _UserLoggedIn() ) { - _ForceLogout(); - - # Authenticate if the user is trying to login via user/pass query args - my ($authed, $msg) = AttemptPasswordAuthentication($ARGS); - - unless ($authed) { - my $m = $HTML::Mason::Commands::m; - - # REST urls get a special 401 response - if ($m->request_comp->path =~ m{^/REST/\d+\.\d+/}) { - $HTML::Mason::Commands::r->content_type("text/plain"); - $m->error_format("text"); - $m->out("RT/$RT::VERSION 401 Credentials required\n"); - $m->out("\n$msg\n") if $msg; - $m->abort; - } - # Specially handle /index.html and /m/index.html so that we get a nicer URL - elsif ( $m->request_comp->path =~ m{^(/m)?/index\.html$} ) { - my $mobile = $1 ? 1 : 0; - my $next = SetNextPage($ARGS); - $m->comp('/NoAuth/Login.html', - next => $next, - actions => [$msg], - mobile => $mobile); - $m->abort; - } - else { - TangentForLogin($ARGS, results => ($msg ? LoginError($msg) : undef)); - } - } - } - - MaybeShowInterstitialCSRFPage($ARGS); - - # now it applies not only to home page, but any dashboard that can be used as a workspace - $HTML::Mason::Commands::session{'home_refresh_interval'} = $ARGS->{'HomeRefreshInterval'} - if ( $ARGS->{'HomeRefreshInterval'} ); - - # Process per-page global callbacks - $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Default', CallbackPage => '/autohandler' ); - - ShowRequestedPage($ARGS); - LogRecordedSQLStatements(RequestData => { - Path => $HTML::Mason::Commands::m->request_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 { - - delete $HTML::Mason::Commands::session{'CurrentUser'}; -} - -sub _UserLoggedIn { - if ( $HTML::Mason::Commands::session{CurrentUser} && $HTML::Mason::Commands::session{'CurrentUser'}->id ) { - return 1; - } else { - return undef; - } - -} - -=head2 LoginError ERROR - -Pushes a login error into the Actions session store and returns the hash key. - -=cut - -sub LoginError { - my $new = shift; - my $key = Digest::MD5::md5_hex( rand(1024) ); - push @{ $HTML::Mason::Commands::session{"Actions"}->{$key} ||= [] }, $new; - $HTML::Mason::Commands::session{'i'}++; - return $key; -} - -=head2 SetNextPage ARGSRef [PATH] - -Intuits and stashes the next page in the sesssion hash. If PATH is -specified, uses that instead of the value of L. Returns -the hash value. - -=cut - -sub SetNextPage { - my $ARGS = shift; - my $next = $_[0] ? $_[0] : IntuitNextPage(); - my $hash = Digest::MD5::md5_hex($next . $$ . rand(1024)); - my $page = { url => $next }; - - # If an explicit URL was passed and we didn't IntuitNextPage, then - # IsPossibleCSRF below is almost certainly unrelated to the actual - # destination. Currently explicit next pages aren't used in RT, but the - # API is available. - if (not $_[0] and RT->Config->Get("RestrictReferrer")) { - # This isn't really CSRF, but the CSRF heuristics are useful for catching - # requests which may have unintended side-effects. - my ($is_csrf, $msg, @loc) = IsPossibleCSRF($ARGS); - if ($is_csrf) { - RT->Logger->notice( - "Marking original destination as having side-effects before redirecting for login.\n" - ."Request: $next\n" - ."Reason: " . HTML::Mason::Commands::loc($msg, @loc) - ); - $page->{'HasSideEffects'} = [$msg, @loc]; - } - } - - $HTML::Mason::Commands::session{'NextPage'}->{$hash} = $page; - $HTML::Mason::Commands::session{'i'}++; - return $hash; -} - -=head2 FetchNextPage HASHKEY - -Returns the stashed next page hashref for the given hash. - -=cut - -sub FetchNextPage { - my $hash = shift || ""; - return $HTML::Mason::Commands::session{'NextPage'}->{$hash}; -} - -=head2 RemoveNextPage HASHKEY - -Removes the stashed next page for the given hash and returns it. - -=cut - -sub RemoveNextPage { - my $hash = shift || ""; - return delete $HTML::Mason::Commands::session{'NextPage'}->{$hash}; -} - -=head2 TangentForLogin ARGSRef [HASH] - -Redirects to C, setting the value of L as -the next page. Takes a hashref of request %ARGS as the first parameter. -Optionally takes all other parameters as a hash which is dumped into query -params. - -=cut - -sub TangentForLogin { - my $ARGS = shift; - my $hash = SetNextPage($ARGS); - my %query = (@_, next => $hash); - - $query{mobile} = 1 - if $HTML::Mason::Commands::m->request_comp->path =~ m{^/m(/|$)}; - - my $login = RT->Config->Get('WebURL') . 'NoAuth/Login.html?'; - $login .= $HTML::Mason::Commands::m->comp('/Elements/QueryString', %query); - Redirect($login); -} - -=head2 TangentForLoginWithError ERROR - -Localizes the passed error message, stashes it with L and then -calls L with the appropriate results key. - -=cut - -sub TangentForLoginWithError { - my $ARGS = shift; - my $key = LoginError(HTML::Mason::Commands::loc(@_)); - TangentForLogin( $ARGS, results => $key ); -} - -=head2 IntuitNextPage - -Attempt to figure out the path to which we should return the user after a -tangent. The current request URL is used, or failing that, the C -configuration variable. - -=cut - -sub IntuitNextPage { - my $req_uri; - - # This includes any query parameters. Redirect will take care of making - # it an absolute URL. - if ($ENV{'REQUEST_URI'}) { - $req_uri = $ENV{'REQUEST_URI'}; - - # collapse multiple leading slashes so the first part doesn't look like - # a hostname of a schema-less URI - $req_uri =~ s{^/+}{/}; - } - - my $next = defined $req_uri ? $req_uri : RT->Config->Get('WebURL'); - - # sanitize $next - my $uri = URI->new($next); - - # You get undef scheme with a relative uri like "/Search/Build.html" - unless (!defined($uri->scheme) || $uri->scheme eq 'http' || $uri->scheme eq 'https') { - $next = RT->Config->Get('WebURL'); - } - - # Make sure we're logging in to the same domain - # You can get an undef authority with a relative uri like "index.html" - my $uri_base_url = URI->new(RT->Config->Get('WebBaseURL')); - unless (!defined($uri->authority) || $uri->authority eq $uri_base_url->authority) { - $next = RT->Config->Get('WebURL'); - } - - return $next; -} - -=head2 MaybeShowInstallModePage - -This function, called exclusively by RT's autohandler, dispatches -a request to RT's Installation workflow, only if Install Mode is enabled in the configuration file. - -If it serves a page, it stops mason processing. Otherwise, mason just keeps running through the autohandler - -=cut - -sub MaybeShowInstallModePage { - return unless RT->InstallMode; - - my $m = $HTML::Mason::Commands::m; - if ( $m->base_comp->path =~ RT->Config->Get('WebNoAuthRegex') ) { - $m->call_next(); - } elsif ( $m->request_comp->path !~ m{^(/+)Install/} ) { - RT::Interface::Web::Redirect( RT->Config->Get('WebURL') . "Install/index.html" ); - } else { - $m->call_next(); - } - $m->abort(); -} - -=head2 MaybeShowNoAuthPage \%ARGS - -This function, called exclusively by RT's autohandler, dispatches -a request to the page a user requested (but only if it matches the "noauth" regex. - -If it serves a page, it stops mason processing. Otherwise, mason just keeps running through the autohandler - -=cut - -sub MaybeShowNoAuthPage { - my $ARGS = shift; - - my $m = $HTML::Mason::Commands::m; - - return unless $m->base_comp->path =~ RT->Config->Get('WebNoAuthRegex'); - - # Don't show the login page to logged in users - Redirect(RT->Config->Get('WebURL')) - if $m->base_comp->path eq '/NoAuth/Login.html' and _UserLoggedIn(); - - # If it's a noauth file, don't ask for auth. - $m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %$ARGS ); - $m->abort; -} - -=head2 MaybeRejectPrivateComponentRequest - -This function will reject calls to private components, like those under -C. If the requested path is a private component then we will -abort with a C<403> error. - -=cut - -sub MaybeRejectPrivateComponentRequest { - my $m = $HTML::Mason::Commands::m; - my $path = $m->request_comp->path; - - # We do not check for dhandler here, because requesting our dhandlers - # directly is okay. Mason will invoke the dhandler with a dhandler_arg of - # 'dhandler'. - - if ($path =~ m{ - / # leading slash - ( Elements | - _elements | # mobile UI - Callbacks | - Widgets | - autohandler | # requesting this directly is suspicious - l (_unsafe)? ) # loc component - ( $ | / ) # trailing slash or end of path - }xi - && $path !~ m{ /RTx/Statistics/\w+/Elements/Chart }xi - ) - { - warn "rejecting private component $path\n"; - $m->abort(403); - } - - 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 -a request to the page a user requested (making sure that unpriviled users -can only see self-service pages. - -=cut - -sub ShowRequestedPage { - my $ARGS = shift; - - my $m = $HTML::Mason::Commands::m; - - # Ensure that the cookie that we send is up-to-date, in case the - # session-id has been modified in any way - SendSessionCookie(); - - # precache all system level rights for the current user - $HTML::Mason::Commands::session{CurrentUser}->PrincipalObj->HasRights( Object => RT->System ); - - # If the user isn't privileged, they can only see SelfService - unless ( $HTML::Mason::Commands::session{'CurrentUser'}->Privileged ) { - - # if the user is trying to access a ticket, redirect them - if ( $m->request_comp->path =~ m{^(/+)Ticket/Display.html} && $ARGS->{'id'} ) { - RT::Interface::Web::Redirect( RT->Config->Get('WebURL') . "SelfService/Display.html?id=" . $ARGS->{'id'} ); - } - - # otherwise, drop the user at the SelfService default page - elsif ( $m->base_comp->path !~ RT->Config->Get('SelfServiceRegex') ) { - RT::Interface::Web::Redirect( RT->Config->Get('WebURL') . "SelfService/" ); - } - - # if user is in SelfService dir let him do anything - else { - $m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %$ARGS ); - } - } else { - $m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %$ARGS ); - } - -} - -sub AttemptExternalAuth { - my $ARGS = shift; - - return unless ( RT->Config->Get('WebExternalAuth') ); - - my $user = $ARGS->{user}; - my $m = $HTML::Mason::Commands::m; - - # If RT is configured for external auth, let's go through and get REMOTE_USER - - # do we actually have a REMOTE_USER equivlent? - if ( RT::Interface::Web::WebCanonicalizeInfo() ) { - my $orig_user = $user; - - $user = RT::Interface::Web::WebCanonicalizeInfo(); - my $load_method = RT->Config->Get('WebExternalGecos') ? 'LoadByGecos' : 'Load'; - - if ( $^O eq 'MSWin32' and RT->Config->Get('WebExternalGecos') ) { - my $NodeName = Win32::NodeName(); - $user =~ s/^\Q$NodeName\E\\//i; - } - - my $next = RemoveNextPage($ARGS->{'next'}); - $next = $next->{'url'} if ref $next; - InstantiateNewSession() unless _UserLoggedIn; - $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new(); - $HTML::Mason::Commands::session{'CurrentUser'}->$load_method($user); - - if ( RT->Config->Get('WebExternalAuto') and not _UserLoggedIn() ) { - - # Create users on-the-fly - my $UserObj = RT::User->new(RT->SystemUser); - my ( $val, $msg ) = $UserObj->Create( - %{ ref RT->Config->Get('AutoCreate') ? RT->Config->Get('AutoCreate') : {} }, - Name => $user, - Gecos => $user, - ); - - if ($val) { - - # now get user specific information, to better create our user. - my $new_user_info = RT::Interface::Web::WebExternalAutoInfo($user); - - # set the attributes that have been defined. - foreach my $attribute ( $UserObj->WritableAttributes ) { - $m->callback( - Attribute => $attribute, - User => $user, - UserInfo => $new_user_info, - CallbackName => 'NewUser', - CallbackPage => '/autohandler' - ); - my $method = "Set$attribute"; - $UserObj->$method( $new_user_info->{$attribute} ) if defined $new_user_info->{$attribute}; - } - $HTML::Mason::Commands::session{'CurrentUser'}->Load($user); - } else { - - # we failed to successfully create the user. abort abort abort. - delete $HTML::Mason::Commands::session{'CurrentUser'}; - - if (RT->Config->Get('WebFallbackToInternalAuth')) { - TangentForLoginWithError($ARGS, 'Cannot create user: [_1]', $msg); - } else { - $m->abort(); - } - } - } - - 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; - - unless ( RT->Config->Get('WebFallbackToInternalAuth') ) { - TangentForLoginWithError($ARGS, 'You are not an authorized user'); - } - } - } elsif ( RT->Config->Get('WebFallbackToInternalAuth') ) { - unless ( defined $HTML::Mason::Commands::session{'CurrentUser'} ) { - # XXX unreachable due to prior defaulting in HandleRequest (check c34d108) - TangentForLoginWithError($ARGS, 'You are not an authorized user'); - } - } else { - - # WebExternalAuth is set, but we don't have a REMOTE_USER. abort - # XXX: we must return AUTH_REQUIRED status or we fallback to - # internal auth here too. - delete $HTML::Mason::Commands::session{'CurrentUser'} - if defined $HTML::Mason::Commands::session{'CurrentUser'}; - } -} - -sub AttemptPasswordAuthentication { - my $ARGS = shift; - return unless defined $ARGS->{user} && defined $ARGS->{pass}; - - my $user_obj = RT::CurrentUser->new(); - $user_obj->Load( $ARGS->{user} ); - - my $m = $HTML::Mason::Commands::m; - - unless ( $user_obj->id && $user_obj->IsPassword( $ARGS->{pass} ) ) { - $RT::Logger->error("FAILED LOGIN for @{[$ARGS->{user}]} from $ENV{'REMOTE_ADDR'}"); - $m->callback( %$ARGS, CallbackName => 'FailedLogin', CallbackPage => '/autohandler' ); - return (0, HTML::Mason::Commands::loc('Your username or password is incorrect')); - } - else { - $RT::Logger->info("Successful login for @{[$ARGS->{user}]} from $ENV{'REMOTE_ADDR'}"); - - # It's important to nab the next page from the session before we blow - # the session away - my $next = RemoveNextPage($ARGS->{'next'}); - $next = $next->{'url'} if ref $next; - - InstantiateNewSession(); - $HTML::Mason::Commands::session{'CurrentUser'} = $user_obj; - - $m->callback( %$ARGS, CallbackName => 'SuccessfulLogin', CallbackPage => '/autohandler' ); - - # Really the only time we don't want to redirect here is if we were - # passed user and pass as query params in the URL. - if ($next) { - Redirect($next); - } - elsif ($ARGS->{'next'}) { - # Invalid hash, but still wants to go somewhere, take them to / - Redirect(RT->Config->Get('WebURL')); - } - - return (1, HTML::Mason::Commands::loc('Logged in')); - } -} - -=head2 LoadSessionFromCookie - -Load or setup a session cookie for the current user. - -=cut - -sub _SessionCookieName { - my $cookiename = "RT_SID_" . RT->Config->Get('rtname'); - $cookiename .= "." . $ENV{'SERVER_PORT'} if $ENV{'SERVER_PORT'}; - return $cookiename; -} - -sub LoadSessionFromCookie { - - my %cookies = CGI::Cookie->fetch; - my $cookiename = _SessionCookieName(); - my $SessionCookie = ( $cookies{$cookiename} ? $cookies{$cookiename}->value : undef ); - tie %HTML::Mason::Commands::session, 'RT::Interface::Web::Session', $SessionCookie; - unless ( $SessionCookie && $HTML::Mason::Commands::session{'_session_id'} eq $SessionCookie ) { - InstantiateNewSession(); - } - if ( int RT->Config->Get('AutoLogoff') ) { - my $now = int( time / 60 ); - my $last_update = $HTML::Mason::Commands::session{'_session_last_update'} || 0; - - if ( $last_update && ( $now - $last_update - RT->Config->Get('AutoLogoff') ) > 0 ) { - InstantiateNewSession(); - } - - # save session on each request when AutoLogoff is turned on - $HTML::Mason::Commands::session{'_session_last_update'} = $now if $now != $last_update; - } -} - -sub InstantiateNewSession { - tied(%HTML::Mason::Commands::session)->delete if tied(%HTML::Mason::Commands::session); - tie %HTML::Mason::Commands::session, 'RT::Interface::Web::Session', undef; - SendSessionCookie(); -} - -sub SendSessionCookie { - my $cookie = CGI::Cookie->new( - -name => _SessionCookieName(), - -value => $HTML::Mason::Commands::session{_session_id}, - -path => RT->Config->Get('WebPath'), - -secure => ( RT->Config->Get('WebSecureCookies') ? 1 : 0 ), - -httponly => ( RT->Config->Get('WebHttpOnlyCookies') ? 1 : 0 ), - ); - - $HTML::Mason::Commands::r->err_headers_out->{'Set-Cookie'} = $cookie->as_string; -} - -=head2 Redirect URL - -This routine ells the current user's browser to redirect to URL. -Additionally, it unties the user's currently active session, helping to avoid -A bug in Apache::Session 1.81 and earlier which clobbers sessions if we try to use -a cached DBI statement handle twice at the same time. - -=cut - -sub Redirect { - my $redir_to = shift; - untie $HTML::Mason::Commands::session; - my $uri = URI->new($redir_to); - my $server_uri = URI->new( _NormalizeHost(RT->Config->Get('WebURL')) ); - - # Make relative URIs absolute from the server host and scheme - $uri->scheme($server_uri->scheme) if not defined $uri->scheme; - if (not defined $uri->host) { - $uri->host($server_uri->host); - $uri->port($server_uri->port); - } - - # 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::CanonicalizeRedirectURLs is true - if ( !RT->Config->Get('CanonicalizeRedirectURLs') - && $uri->host eq $server_uri->host - && $uri->port eq $server_uri->port ) - { - if ( defined $ENV{HTTPS} and $ENV{'HTTPS'} eq 'on' ) { - $uri->scheme('https'); - } else { - $uri->scheme('http'); - } - - # [rt3.fsck.com #12716] Apache recommends use of $SERVER_HOST - $uri->host( $ENV{'SERVER_HOST'} || $ENV{'HTTP_HOST'} || $ENV{'SERVER_NAME'}); - $uri->port( $ENV{'SERVER_PORT'} ); - } - - # not sure why, but on some systems without this call mason doesn't - # set status to 302, but 200 instead and people see blank pages - $HTML::Mason::Commands::r->status(302); - - # Perlbal expects a status message, but Mason's default redirect status - # doesn't provide one. See also rt.cpan.org #36689. - $HTML::Mason::Commands::m->redirect( $uri->canonical, "302 Found" ); - - $HTML::Mason::Commands::m->abort; -} - -=head2 CacheControlExpiresHeaders - -set both Cache-Control and Expires http headers - -=cut - -sub CacheControlExpiresHeaders { - my %args = @_; - - my $Visibility = 'private'; - if ( ! defined $args{Time} ) { - $args{Time} = 0; - } elsif ( $args{Time} eq 'no-cache' ) { - $args{Time} = 0; - } elsif ( $args{Time} eq 'forever' ) { - $args{Time} = 30 * 24 * 60 * 60; - $Visibility = 'public'; - } - - my $CacheControl = $args{Time} - ? sprintf "max-age=%d, %s", $args{Time}, $Visibility - : 'no-cache' - ; - $HTML::Mason::Commands::r->headers_out->{'Cache-Control'} = $CacheControl; - - my $expires = RT::Date->new(RT->SystemUser); - $expires->SetToNow; - $expires->AddSeconds( $args{Time} ) if $args{Time}; - - $HTML::Mason::Commands::r->headers_out->{'Expires'} = $expires->RFC2616; -} - -=head2 StaticFileHeaders - -Send the browser a few headers to try to get it to (somewhat agressively) -cache RT's static Javascript and CSS files. - -This routine could really use _accurate_ heuristics. (XXX TODO) - -=cut - -sub StaticFileHeaders { - my $date = RT::Date->new(RT->SystemUser); - - # remove any cookie headers -- if it is cached publicly, it - # shouldn't include anyone's cookie! - delete $HTML::Mason::Commands::r->err_headers_out->{'Set-Cookie'}; - - # Expire things in a month. - CacheControlExpiresHeaders( Time => 'forever' ); - - # if we set 'Last-Modified' then browser request a comp using 'If-Modified-Since' - # request, but we don't handle it and generate full reply again - # Last modified at server start time - # $date->Set( Value => $^T ); - # $HTML::Mason::Commands::r->headers_out->{'Last-Modified'} = $date->RFC2616; -} - -=head2 ComponentPathIsSafe PATH - -Takes C and returns a boolean indicating that the user-specified partial -component path is safe. - -Currently "safe" means that the path does not start with a dot (C<.>), does -not contain a slash-dot C, and does not contain any nulls. - -=cut - -sub ComponentPathIsSafe { - my $self = shift; - my $path = shift; - return($path !~ m{(?:^|/)\.} and $path !~ m{\0}); -} - -=head2 PathIsSafe - -Takes a C<< Path => path >> and returns a boolean indicating that -the path is safely within RT's control or not. The path I be -relative. - -This function does not consult the filesystem at all; it is merely -a logical sanity checking of the path. This explicitly does not handle -symlinks; if you have symlinks in RT's webroot pointing outside of it, -then we assume you know what you are doing. - -=cut - -sub PathIsSafe { - my $self = shift; - my %args = @_; - my $path = $args{Path}; - - # Get File::Spec to clean up extra /s, ./, etc - my $cleaned_up = File::Spec->canonpath($path); - - if (!defined($cleaned_up)) { - $RT::Logger->info("Rejecting path that canonpath doesn't understand: $path"); - return 0; - } - - # Forbid too many ..s. We can't just sum then check because - # "../foo/bar/baz" should be illegal even though it has more - # downdirs than updirs. So as soon as we get a negative score - # (which means "breaking out" of the top level) we reject the path. - - my @components = split '/', $cleaned_up; - my $score = 0; - for my $component (@components) { - if ($component eq '..') { - $score--; - if ($score < 0) { - $RT::Logger->info("Rejecting unsafe path: $path"); - return 0; - } - } - elsif ($component eq '.' || $component eq '') { - # these two have no effect on $score - } - else { - $score++; - } - } - - return 1; -} - -=head2 SendStaticFile - -Takes a File => path and a Type => Content-type - -If Type isn't provided and File is an image, it will -figure out a sane Content-type, otherwise it will -send application/octet-stream - -Will set caching headers using StaticFileHeaders - -=cut - -sub SendStaticFile { - my $self = shift; - my %args = @_; - my $file = $args{File}; - my $type = $args{Type}; - my $relfile = $args{RelativeFile}; - - if (defined($relfile) && !$self->PathIsSafe(Path => $relfile)) { - $HTML::Mason::Commands::r->status(400); - $HTML::Mason::Commands::m->abort; - } - - $self->StaticFileHeaders(); - - unless ($type) { - if ( $file =~ /\.(gif|png|jpe?g)$/i ) { - $type = "image/$1"; - $type =~ s/jpg/jpeg/gi; - } - $type ||= "application/octet-stream"; - } - $HTML::Mason::Commands::r->content_type($type); - open( my $fh, '<', $file ) or die "couldn't open file: $!"; - binmode($fh); - { - local $/ = \16384; - $HTML::Mason::Commands::m->out($_) while (<$fh>); - $HTML::Mason::Commands::m->flush_buffer; - } - 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|Mobile)/io && !$HTML::Mason::Commands::session{'NotMobile'}) { - return 1; -} else { - return undef; -} - -} - - -sub StripContent { - my %args = @_; - my $content = $args{Content}; - return '' unless $content; - - # Make the content have no 'weird' newlines in it - $content =~ s/\r+\n/\n/g; - - my $return_content = $content; - - my $html = $args{ContentType} && $args{ContentType} eq "text/html"; - my $sigonly = $args{StripSignature}; - - # massage content to easily detect if there's any real content - $content =~ s/\s+//g; # yes! remove all the spaces - if ( $html ) { - # remove html version of spaces and newlines - $content =~ s! !!g; - $content =~ s!
!!g; - } - - # Filter empty content when type is text/html - return '' if $html && $content !~ /\S/; - - # If we aren't supposed to strip the sig, just bail now. - return $return_content unless $sigonly; - - # Find the signature - my $sig = $args{'CurrentUser'}->UserObj->Signature || ''; - $sig =~ s/\s+//g; - - # Check for plaintext sig - 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 escapting that FCKEditor - # uses. - $sig =~ s/&/&/g; - $sig =~ s//>/g; - $sig =~ s/"/"/g; - $sig =~ s/'/'/g; - return '' if $html and $content =~ m{^(?:

)?(--)?\Q$sig\E(?:

)?$}s; - - # Pass it through - return $return_content; -} - -sub DecodeARGS { - my $ARGS = shift; - - %{$ARGS} = map { - - # if they've passed multiple values, they'll be an array. if they've - # passed just one, a scalar whatever they are, mark them as utf8 - my $type = ref($_); - ( !$type ) - ? Encode::is_utf8($_) - ? $_ - : Encode::decode( 'UTF-8' => $_, Encode::FB_PERLQQ ) - : ( $type eq 'ARRAY' ) - ? [ map { ( ref($_) or Encode::is_utf8($_) ) ? $_ : Encode::decode( 'UTF-8' => $_, Encode::FB_PERLQQ ) } - @$_ ] - : ( $type eq 'HASH' ) - ? { map { ( ref($_) or Encode::is_utf8($_) ) ? $_ : Encode::decode( 'UTF-8' => $_, Encode::FB_PERLQQ ) } - %$_ } - : $_ - } %$ARGS; -} - -sub PreprocessTimeUpdates { - my $ARGS = shift; - - # Later in the code we use - # $m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %ARGS ); - # instead of $m->call_next to avoid problems with UTF8 keys in arguments. - # The call_next method pass through original arguments and if you have - # an argument with unicode key then in a next component you'll get two - # records in the args hash: one with key without UTF8 flag and another - # with the flag, which may result into errors. "{ base_comp => $m->request_comp }" - # is copied from mason's source to get the same results as we get from - # call_next method, this feature is not documented, so we just leave it - # here to avoid possible side effects. - - # This code canonicalizes time inputs in hours into minutes - foreach my $field ( keys %$ARGS ) { - next unless $field =~ /^(.*)-TimeUnits$/i && $ARGS->{$1}; - my $local = $1; - $ARGS->{$local} =~ s{\b (?: (\d+) \s+ )? (\d+)/(\d+) \b} - {($1 || 0) + $3 ? $2 / $3 : 0}xe; - if ( $ARGS->{$field} && $ARGS->{$field} =~ /hours/i ) { - $ARGS->{$local} *= 60; - } - delete $ARGS->{$field}; - } - -} - -sub MaybeEnableSQLStatementLog { - - my $log_sql_statements = RT->Config->Get('StatementLog'); - - if ($log_sql_statements) { - $RT::Handle->ClearSQLStatementLog; - $RT::Handle->LogSQLStatements(1); - } - -} - -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; - if ( ref $bind ) { - @bind = @{$bind}; - } else { - - # Older DBIx-SB - $duration = $bind; - } - $RT::Logger->log( - level => $log_sql_statements, - message => "SQL(" - . sprintf( "%.6f", $duration ) - . "s): $sql;" - . ( @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; - - my $port = $ENV{SERVER_PORT}; - my $host = $ENV{HTTP_X_FORWARDED_HOST} || $ENV{HTTP_X_FORWARDED_SERVER} - || $ENV{HTTP_HOST} || $ENV{SERVER_NAME}; - ($host, $port) = ($1, $2) if $host =~ /^(.*?):(\d+)$/; - - if ( $port != RT->Config->Get('WebPort') and not $ENV{'rt.explicit_port'}) { - $RT::Logger->warn("The requested port ($port) does NOT match the configured WebPort ($RT::WebPort). " - ."Perhaps you should Set(\$WebPort, $port); in RT_SiteConfig.pm, " - ."otherwise your internal links may be broken."); - } - - if ( $host ne RT->Config->Get('WebDomain') ) { - $RT::Logger->warn("The requested 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."); - } - - return; #next warning flooding our logs, doesn't seem applicable to our use - # (SCRIPT_NAME is the full path, WebPath is just the beginning) - #in vanilla RT does something eat the local part of SCRIPT_NAME 1st? - - # Unfortunately, there is no reliable way to get the _path_ that was - # requested at the proxy level; simply disable this warning if we're - # proxied and there's a mismatch. - my $proxied = $ENV{HTTP_X_FORWARDED_HOST} || $ENV{HTTP_X_FORWARDED_SERVER}; - if ($ENV{SCRIPT_NAME} ne RT->Config->Get('WebPath') and not $proxied) { - $RT::Logger->warn("The requested path ($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; -} - -our %is_whitelisted_component = ( - # The RSS feed embeds an auth token in the path, but query - # information for the search. Because it's a straight-up read, in - # addition to embedding its own auth, it's fine. - '/NoAuth/rss/dhandler' => 1, - - # While these can be used for denial-of-service against RT - # (construct a very inefficient query and trick lots of users into - # running them against RT) it's incredibly useful to be able to link - # to a search result (or chart) or bookmark a result page. - '/Search/Results.html' => 1, - '/Search/Simple.html' => 1, - '/m/tickets/search' => 1, - '/Search/Chart.html' => 1, - - # This page takes Attachment and Transaction argument to figure - # out what to show, but it's read only and will deny information if you - # don't have ShowOutgoingEmail. - '/Ticket/ShowEmailRecord.html' => 1, -); - -# Components which are blacklisted from automatic, argument-based whitelisting. -# These pages are not idempotent when called with just an id. -our %is_blacklisted_component = ( - # Takes only id and toggles bookmark state - '/Helpers/Toggle/TicketBookmark' => 1, -); - -sub IsCompCSRFWhitelisted { - my $comp = shift; - my $ARGS = shift; - - return 1 if $is_whitelisted_component{$comp}; - - my %args = %{ $ARGS }; - - # If the user specifies a *correct* user and pass then they are - # golden. This acts on the presumption that external forms may - # hardcode a username and password -- if a malicious attacker knew - # both already, CSRF is the least of your problems. - my $AllowLoginCSRF = not RT->Config->Get('RestrictReferrerLogin'); - if ($AllowLoginCSRF and defined($args{user}) and defined($args{pass})) { - my $user_obj = RT::CurrentUser->new(); - $user_obj->Load($args{user}); - return 1 if $user_obj->id && $user_obj->IsPassword($args{pass}); - - delete $args{user}; - delete $args{pass}; - } - - # Some pages aren't idempotent even with safe args like id; blacklist - # them from the automatic whitelisting below. - return 0 if $is_blacklisted_component{$comp}; - - # Eliminate arguments that do not indicate an effectful request. - # For example, "id" is acceptable because that is how RT retrieves a - # record. - delete $args{id}; - - # If they have a results= from MaybeRedirectForResults, that's also fine. - delete $args{results}; - - # The homepage refresh, which uses the Refresh header, doesn't send - # a referer in most browsers; whitelist the one parameter it reloads - # with, HomeRefreshInterval, which is safe - delete $args{HomeRefreshInterval}; - - # The NotMobile flag is fine for any page; it's only used to toggle a flag - # in the session related to which interface you get. - delete $args{NotMobile}; - - # If there are no arguments, then it's likely to be an idempotent - # request, which are not susceptible to CSRF - return 1 if !%args; - - return 0; -} - -sub IsRefererCSRFWhitelisted { - my $referer = _NormalizeHost(shift); - my $base_url = _NormalizeHost(RT->Config->Get('WebBaseURL')); - $base_url = $base_url->host_port; - - my $configs; - for my $config ( $base_url, RT->Config->Get('ReferrerWhitelist') ) { - push @$configs,$config; - - my $host_port = $referer->host_port; - if ($config =~ /\*/) { - # Turn a literal * into a domain component or partial component match. - # Refer to http://tools.ietf.org/html/rfc2818#page-5 - my $regex = join "[a-zA-Z0-9\-]*", - map { quotemeta($_) } - split /\*/, $config; - - return 1 if $host_port =~ /^$regex$/i; - } else { - return 1 if $host_port eq $config; - } - } - - return (0,$referer,$configs); -} - -=head3 _NormalizeHost - -Takes a URI and creates a URI object that's been normalized -to handle common problems such as localhost vs 127.0.0.1 - -=cut - -sub _NormalizeHost { - my $s = shift; - $s = "http://$s" unless $s =~ /^http/i; - my $uri= URI->new($s); - $uri->host('127.0.0.1') if $uri->host eq 'localhost'; - - return $uri; - -} - -sub IsPossibleCSRF { - my $ARGS = shift; - - # If first request on this session is to a REST endpoint, then - # whitelist the REST endpoints -- and explicitly deny non-REST - # endpoints. We do this because using a REST cookie in a browser - # would open the user to CSRF attacks to the REST endpoints. - my $path = $HTML::Mason::Commands::r->path_info; - $HTML::Mason::Commands::session{'REST'} = $path =~ m{^/+REST/\d+\.\d+(/|$)} - unless defined $HTML::Mason::Commands::session{'REST'}; - - if ($HTML::Mason::Commands::session{'REST'}) { - return 0 if $path =~ m{^/+REST/\d+\.\d+(/|$)}; - my $why = < $details ); - } - - return 0 if IsCompCSRFWhitelisted( - $HTML::Mason::Commands::m->request_comp->path, - $ARGS - ); - - # if there is no Referer header then assume the worst - return (1, - "your browser did not supply a Referrer header", # loc - ) if !$ENV{HTTP_REFERER}; - - my ($whitelisted, $browser, $configs) = IsRefererCSRFWhitelisted($ENV{HTTP_REFERER}); - return 0 if $whitelisted; - - if ( @$configs > 1 ) { - return (1, - "the Referrer header supplied by your browser ([_1]) is not allowed by RT's configured hostname ([_2]) or whitelisted hosts ([_3])", # loc - $browser->host_port, - shift @$configs, - join(', ', @$configs) ); - } - - return (1, - "the Referrer header supplied by your browser ([_1]) is not allowed by RT's configured hostname ([_2])", # loc - $browser->host_port, - $configs->[0]); -} - -sub ExpandCSRFToken { - my $ARGS = shift; - - my $token = delete $ARGS->{CSRF_Token}; - return unless $token; - - my $data = $HTML::Mason::Commands::session{'CSRF'}{$token}; - return unless $data; - return unless $data->{path} eq $HTML::Mason::Commands::r->path_info; - - my $user = $HTML::Mason::Commands::session{'CurrentUser'}->UserObj; - return unless $user->ValidateAuthString( $data->{auth}, $token ); - - %{$ARGS} = %{$data->{args}}; - $HTML::Mason::Commands::DECODED_ARGS = $ARGS; - - # We explicitly stored file attachments with the request, but not in - # the session yet, as that would itself be an attack. Put them into - # the session now, so they'll be visible. - if ($data->{attach}) { - my $filename = $data->{attach}{filename}; - my $mime = $data->{attach}{mime}; - $HTML::Mason::Commands::session{'Attachments'}{$filename} - = $mime; - } - - return 1; -} - -sub StoreRequestToken { - my $ARGS = shift; - - my $token = Digest::MD5::md5_hex(time . {} . $$ . rand(1024)); - my $user = $HTML::Mason::Commands::session{'CurrentUser'}->UserObj; - my $data = { - auth => $user->GenerateAuthString( $token ), - path => $HTML::Mason::Commands::r->path_info, - args => $ARGS, - }; - if ($ARGS->{Attach}) { - my $attachment = HTML::Mason::Commands::MakeMIMEEntity( AttachmentFieldName => 'Attach' ); - my $file_path = delete $ARGS->{'Attach'}; - $data->{attach} = { - filename => Encode::decode_utf8("$file_path"), - mime => $attachment, - }; - } - - $HTML::Mason::Commands::session{'CSRF'}->{$token} = $data; - $HTML::Mason::Commands::session{'i'}++; - return $token; -} - -sub MaybeShowInterstitialCSRFPage { - my $ARGS = shift; - - return unless RT->Config->Get('RestrictReferrer'); - - # Deal with the form token provided by the interstitial, which lets - # browsers which never set referer headers still use RT, if - # painfully. This blows values into ARGS - return if ExpandCSRFToken($ARGS); - - my ($is_csrf, $msg, @loc) = IsPossibleCSRF($ARGS); - return if !$is_csrf; - - $RT::Logger->notice("Possible CSRF: ".RT::CurrentUser->new->loc($msg, @loc)); - - my $token = StoreRequestToken($ARGS); - $HTML::Mason::Commands::m->comp( - '/Elements/CSRF', - OriginalURL => RT->Config->Get('WebPath') . $HTML::Mason::Commands::r->path_info, - Reason => HTML::Mason::Commands::loc( $msg, @loc ), - Token => $token, - ); - # Calls abort, never gets here -} - -our @POTENTIAL_PAGE_ACTIONS = ( - qr'/Ticket/Create.html' => "create a ticket", # loc - qr'/Ticket/' => "update a ticket", # loc - qr'/Admin/' => "modify RT's configuration", # loc - qr'/Approval/' => "update an approval", # loc - qr'/Articles/' => "update an article", # loc - qr'/Dashboards/' => "modify a dashboard", # loc - qr'/m/ticket/' => "update a ticket", # loc - qr'Prefs' => "modify your preferences", # loc - qr'/Search/' => "modify or access a search", # loc - qr'/SelfService/Create' => "create a ticket", # loc - qr'/SelfService/' => "update a ticket", # loc -); - -sub PotentialPageAction { - my $page = shift; - my @potentials = @POTENTIAL_PAGE_ACTIONS; - while (my ($pattern, $result) = splice @potentials, 0, 2) { - return HTML::Mason::Commands::loc($result) - if $page =~ $pattern; - } - return ""; -} - -package HTML::Mason::Commands; - -use vars qw/$r $m %session/; - -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 - -loc is a nice clean global routine which calls $session{'CurrentUser'}->loc() -with whatever it's called with. If there is no $session{'CurrentUser'}, -it creates a temporary user, so we have something to get a localisation handle -through - -=cut - -sub loc { - - if ( $session{'CurrentUser'} - && UNIVERSAL::can( $session{'CurrentUser'}, 'loc' ) ) - { - return ( $session{'CurrentUser'}->loc(@_) ); - } elsif ( - my $u = eval { - RT::CurrentUser->new(); - } - ) - { - return ( $u->loc(@_) ); - } else { - - # pathetic case -- SystemUser is gone. - return $_[0]; - } -} - - - -=head2 loc_fuzzy STRING - -loc_fuzzy is for handling localizations of messages that may already -contain interpolated variables, typically returned from libraries -outside RT's control. It takes the message string and extracts the -variable array automatically by matching against the candidate entries -inside the lexicon file. - -=cut - -sub loc_fuzzy { - my $msg = shift; - - if ( $session{'CurrentUser'} - && UNIVERSAL::can( $session{'CurrentUser'}, 'loc' ) ) - { - return ( $session{'CurrentUser'}->loc_fuzzy($msg) ); - } else { - my $u = RT::CurrentUser->new( RT->SystemUser->Id ); - return ( $u->loc_fuzzy($msg) ); - } -} - - -# Error - calls Error and aborts -sub Abort { - my $why = shift; - my %args = @_; - - if ( $session{'ErrorDocument'} - && $session{'ErrorDocumentType'} ) - { - $r->content_type( $session{'ErrorDocumentType'} ); - $m->comp( $session{'ErrorDocument'}, Why => $why, %args ); - $m->abort; - } else { - $m->comp( "/Elements/Error", Why => $why, %args ); - $m->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. - -Cs matching C are let through. - -This is a no-op if the C option isn't enabled. - -=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 - -Create a new ticket, using Mason's %ARGS. returns @results. - -=cut - -sub CreateTicket { - my %ARGS = (@_); - - my (@Actions); - - my $Ticket = RT::Ticket->new( $session{'CurrentUser'} ); - - my $Queue = RT::Queue->new( $session{'CurrentUser'} ); - unless ( $Queue->Load( $ARGS{'Queue'} ) ) { - Abort('Queue not found'); - } - - unless ( $Queue->CurrentUserHasRight('CreateTicket') ) { - Abort('You have no permission to create tickets in that queue.'); - } - - my $due; - if ( defined $ARGS{'Due'} and $ARGS{'Due'} =~ /\S/ ) { - $due = RT::Date->new( $session{'CurrentUser'} ); - $due->Set( Format => 'unknown', Value => $ARGS{'Due'} ); - } - my $starts; - if ( defined $ARGS{'Starts'} and $ARGS{'Starts'} =~ /\S/ ) { - $starts = RT::Date->new( $session{'CurrentUser'} ); - $starts->Set( Format => 'unknown', Value => $ARGS{'Starts'} ); - } - - my $sigless = RT::Interface::Web::StripContent( - Content => $ARGS{Content}, - ContentType => $ARGS{ContentType}, - StripSignature => 1, - CurrentUser => $session{'CurrentUser'}, - ); - - my $MIMEObj = MakeMIMEEntity( - Subject => $ARGS{'Subject'}, - From => $ARGS{'From'}, - Cc => $ARGS{'Cc'}, - Body => $sigless, - Type => $ARGS{'ContentType'}, - Interface => RT::Interface::Web::MobileClient() ? 'Mobile' : 'Web', - ); - - if ( $ARGS{'Attachments'} ) { - my $rv = $MIMEObj->make_multipart; - $RT::Logger->error("Couldn't make multipart message") - if !$rv || $rv !~ /^(?:DONE|ALREADY)$/; - - foreach ( map $ARGS{Attachments}->{$_}, sort keys %{ $ARGS{'Attachments'} } ) { - unless ($_) { - $RT::Logger->error("Couldn't add empty attachemnt"); - next; - } - $MIMEObj->add_part($_); - } - } - - for my $argument (qw(Encrypt Sign)) { - $MIMEObj->head->replace( "X-RT-$argument" => $ARGS{$argument} ? 1 : 0 ); - } - - my %create_args = ( - Type => $ARGS{'Type'} || 'ticket', - Queue => $ARGS{'Queue'}, - Owner => $ARGS{'Owner'}, - - # note: name change - Requestor => $ARGS{'Requestors'}, - Cc => $ARGS{'Cc'}, - AdminCc => $ARGS{'AdminCc'}, - InitialPriority => $ARGS{'InitialPriority'}, - FinalPriority => $ARGS{'FinalPriority'}, - TimeLeft => $ARGS{'TimeLeft'}, - TimeEstimated => $ARGS{'TimeEstimated'}, - TimeWorked => $ARGS{'TimeWorked'}, - Subject => $ARGS{'Subject'}, - Status => $ARGS{'Status'}, - Due => $due ? $due->ISO : undef, - Starts => $starts ? $starts->ISO : undef, - MIMEObj => $MIMEObj - ); - - my @txn_squelch; - foreach my $type (qw(Requestor Cc AdminCc)) { - push @txn_squelch, map $_->address, Email::Address->parse( $create_args{$type} ) - if grep $_ eq $type || $_ eq ( $type . 's' ), @{ $ARGS{'SkipNotification'} || [] }; - } - $create_args{TransSquelchMailTo} = \@txn_squelch - if @txn_squelch; - - if ( $ARGS{'AttachTickets'} ) { - require RT::Action::SendEmail; - RT::Action::SendEmail->AttachTickets( RT::Action::SendEmail->AttachTickets, - ref $ARGS{'AttachTickets'} - ? @{ $ARGS{'AttachTickets'} } - : ( $ARGS{'AttachTickets'} ) ); - } - - foreach my $arg ( keys %ARGS ) { - next if $arg =~ /-(?:Magic|Category)$/; - - if ( $arg =~ /^Object-RT::Transaction--CustomField-/ ) { - $create_args{$arg} = $ARGS{$arg}; - } - - # Object-RT::Ticket--CustomField-3-Values - elsif ( $arg =~ /^Object-RT::Ticket--CustomField-(\d+)/ ) { - my $cfid = $1; - - my $cf = RT::CustomField->new( $session{'CurrentUser'} ); - $cf->SetContextObject( $Queue ); - $cf->Load($cfid); - unless ( $cf->id ) { - $RT::Logger->error( "Couldn't load custom field #" . $cfid ); - next; - } - - if ( $arg =~ /-Upload$/ ) { - $create_args{"CustomField-$cfid"} = _UploadedFile($arg); - next; - } - - my $type = $cf->Type; - - my @values = (); - if ( ref $ARGS{$arg} eq 'ARRAY' ) { - @values = @{ $ARGS{$arg} }; - } elsif ( $type =~ /text/i ) { - @values = ( $ARGS{$arg} ); - } else { - no warnings 'uninitialized'; - @values = split /\r*\n/, $ARGS{$arg}; - } - @values = grep length, map { - s/\r+\n/\n/g; - s/^\s+//; - s/\s+$//; - $_; - } - grep defined, @values; - - $create_args{"CustomField-$cfid"} = \@values; - } - } - - # turn new link lists into arrays, and pass in the proper arguments - my %map = ( - 'new-DependsOn' => 'DependsOn', - 'DependsOn-new' => 'DependedOnBy', - 'new-MemberOf' => 'Parents', - 'MemberOf-new' => 'Children', - 'new-RefersTo' => 'RefersTo', - 'RefersTo-new' => 'ReferredToBy', - ); - foreach my $key ( keys %map ) { - next unless $ARGS{$key}; - $create_args{ $map{$key} } = [ grep $_, split ' ', $ARGS{$key} ]; - - } - - my ( $id, $Trans, $ErrMsg ) = $Ticket->Create(%create_args); - unless ($id) { - Abort($ErrMsg); - } - - push( @Actions, split( "\n", $ErrMsg ) ); - unless ( $Ticket->CurrentUserHasRight('ShowTicket') ) { - Abort( "No permission to view newly created ticket #" . $Ticket->id . "." ); - } - return ( $Ticket, @Actions ); - -} - - - -=head2 LoadTicket id - -Takes a ticket id as its only variable. if it's handed an array, it takes -the first value. - -Returns an RT::Ticket object as the current user. - -=cut - -sub LoadTicket { - my $id = shift; - - if ( ref($id) eq "ARRAY" ) { - $id = $id->[0]; - } - - unless ($id) { - Abort("No ticket specified"); - } - - my $Ticket = RT::Ticket->new( $session{'CurrentUser'} ); - $Ticket->Load($id); - unless ( $Ticket->id ) { - Abort("Could not load ticket $id"); - } - return $Ticket; -} - - - -=head2 ProcessUpdateMessage - -Takes paramhash with fields ARGSRef, TicketObj and SkipSignatureOnly. - -Don't write message if it only contains current user's signature and -SkipSignatureOnly argument is true. Function anyway adds attachments -and updates time worked field even if skips message. The default value -is true. - -=cut - -# change from stock: if txn custom fields are set but there's no content -# or attachment, create a Touch txn instead of doing nothing - -sub ProcessUpdateMessage { - - my %args = ( - ARGSRef => undef, - TicketObj => undef, - SkipSignatureOnly => 1, - @_ - ); - - if ( $args{ARGSRef}->{'UpdateAttachments'} - && !keys %{ $args{ARGSRef}->{'UpdateAttachments'} } ) - { - delete $args{ARGSRef}->{'UpdateAttachments'}; - } - - # Strip the signature - $args{ARGSRef}->{UpdateContent} = RT::Interface::Web::StripContent( - Content => $args{ARGSRef}->{UpdateContent}, - ContentType => $args{ARGSRef}->{UpdateContentType}, - StripSignature => $args{SkipSignatureOnly}, - CurrentUser => $args{'TicketObj'}->CurrentUser, - ); - - my %txn_customfields; - - foreach my $key ( keys %{ $args{ARGSRef} } ) { - if ( $key =~ /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ ) { - next if $key =~ /(TimeUnits|Magic)$/; - $txn_customfields{$key} = $args{ARGSRef}->{$key}; - } - } - - # If, after stripping the signature, we have no message, create a - # Touch transaction if necessary - if ( not $args{ARGSRef}->{'UpdateAttachments'} - and not length $args{ARGSRef}->{'UpdateContent'} ) - { - #if ( $args{ARGSRef}->{'UpdateTimeWorked'} ) { - # $args{ARGSRef}->{TimeWorked} = $args{TicketObj}->TimeWorked + - # delete $args{ARGSRef}->{'UpdateTimeWorked'}; - # } - - my $timetaken = $args{ARGSRef}->{'UpdateTimeWorked'}; - if ( $timetaken or grep {length $_} values %txn_customfields ) { - my ( $Transaction, $Description, $Object ) = - $args{TicketObj}->Touch( - CustomFields => \%txn_customfields, - TimeTaken => $timetaken - ); - return $Description; - } - return; - } - - if ( $args{ARGSRef}->{'UpdateSubject'} eq ($args{'TicketObj'}->Subject || '') ) { - $args{ARGSRef}->{'UpdateSubject'} = undef; - } - - my $Message = MakeMIMEEntity( - Subject => $args{ARGSRef}->{'UpdateSubject'}, - Body => $args{ARGSRef}->{'UpdateContent'}, - Type => $args{ARGSRef}->{'UpdateContentType'}, - Interface => RT::Interface::Web::MobileClient() ? 'Mobile' : 'Web', - ); - - $Message->head->replace( 'Message-ID' => Encode::encode_utf8( - RT::Interface::Email::GenMessageId( Ticket => $args{'TicketObj'} ) - ) ); - my $old_txn = RT::Transaction->new( $session{'CurrentUser'} ); - if ( $args{ARGSRef}->{'QuoteTransaction'} ) { - $old_txn->Load( $args{ARGSRef}->{'QuoteTransaction'} ); - } else { - $old_txn = $args{TicketObj}->Transactions->First(); - } - - if ( my $msg = $old_txn->Message->First ) { - RT::Interface::Email::SetInReplyTo( - Message => $Message, - InReplyTo => $msg - ); - } - - if ( $args{ARGSRef}->{'UpdateAttachments'} ) { - $Message->make_multipart; - $Message->add_part($_) foreach map $args{ARGSRef}->{UpdateAttachments}{$_}, - sort keys %{ $args{ARGSRef}->{'UpdateAttachments'} }; - } - - if ( $args{ARGSRef}->{'AttachTickets'} ) { - require RT::Action::SendEmail; - RT::Action::SendEmail->AttachTickets( RT::Action::SendEmail->AttachTickets, - ref $args{ARGSRef}->{'AttachTickets'} - ? @{ $args{ARGSRef}->{'AttachTickets'} } - : ( $args{ARGSRef}->{'AttachTickets'} ) ); - } - - my %message_args = ( - Sign => ( $args{ARGSRef}->{'Sign'} ? 1 : 0 ), - Encrypt => ( $args{ARGSRef}->{'Encrypt'} ? 1 : 0 ), - MIMEObj => $Message, - TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'}, - CustomFields => \%txn_customfields, - ); - - _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 @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 @txn_squelch, map $_->address, Email::Address->parse( $message_args->{Requestor} ); - push @txn_squelch, $args{TicketObj}->Requestors->MemberEmailAddresses; - } - - 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"; - } else { - $message_args->{$var} = $value; - } - } - } -} - -sub ProcessAttachments { - my %args = ( - ARGSRef => {}, - @_ - ); - - my $ARGSRef = $args{ARGSRef} || {}; - # deal with deleting uploaded attachments - foreach my $key ( keys %$ARGSRef ) { - if ( $key =~ m/^DeleteAttach-(.+)$/ ) { - delete $session{'Attachments'}{$1}; - } - $session{'Attachments'} = { %{ $session{'Attachments'} || {} } }; - } - - # store the uploaded attachment in session - if ( defined $ARGSRef->{'Attach'} && length $ARGSRef->{'Attach'} ) - { # attachment? - my $attachment = MakeMIMEEntity( AttachmentFieldName => 'Attach' ); - - my $file_path = Encode::decode_utf8("$ARGSRef->{'Attach'}"); - $session{'Attachments'} = - { %{ $session{'Attachments'} || {} }, $file_path => $attachment, }; - } - - # delete temporary storage entry to make WebUI clean - unless ( keys %{ $session{'Attachments'} } and $ARGSRef->{'UpdateAttach'} ) - { - delete $session{'Attachments'}; - } -} - - -=head2 MakeMIMEEntity PARAMHASH - -Takes a paramhash Subject, Body and AttachmentFieldName. - -Also takes Form, Cc and Type as optional paramhash keys. - - Returns a MIME::Entity. - -=cut - -sub MakeMIMEEntity { - - #TODO document what else this takes. - my %args = ( - Subject => undef, - From => undef, - Cc => undef, - Body => undef, - AttachmentFieldName => undef, - Type => undef, - Interface => 'API', - @_, - ); - my $Message = MIME::Entity->build( - Type => 'multipart/mixed', - "Message-Id" => Encode::encode_utf8( RT::Interface::Email::GenMessageId ), - "X-RT-Interface" => $args{Interface}, - map { $_ => Encode::encode_utf8( $args{ $_} ) } - grep defined $args{$_}, qw(Subject From Cc) - ); - - if ( defined $args{'Body'} && length $args{'Body'} ) { - - # Make the update content have no 'weird' newlines in it - $args{'Body'} =~ s/\r\n/\n/gs; - - $Message->attach( - Type => $args{'Type'} || 'text/plain', - Charset => 'UTF-8', - Data => $args{'Body'}, - ); - } - - if ( $args{'AttachmentFieldName'} ) { - - my $cgi_object = $m->cgi_object; - my $filehandle = $cgi_object->upload( $args{'AttachmentFieldName'} ); - if ( defined $filehandle && length $filehandle ) { - - my ( @content, $buffer ); - while ( my $bytesread = read( $filehandle, $buffer, 4096 ) ) { - push @content, $buffer; - } - - my $uploadinfo = $cgi_object->uploadInfo($filehandle); - - my $filename = "$filehandle"; - $filename =~ s{^.*[\\/]}{}; - - $Message->attach( - Type => $uploadinfo->{'Content-Type'}, - Filename => $filename, - Data => \@content, - ); - if ( !$args{'Subject'} && !( defined $args{'Body'} && length $args{'Body'} ) ) { - $Message->head->set( 'Subject' => $filename ); - } - - # Attachment parts really shouldn't get a Message-ID or "interface" - $Message->head->delete('Message-ID'); - $Message->head->delete('X-RT-Interface'); - } - } - - $Message->make_singlepart; - - RT::I18N::SetMIMEEntityToUTF8($Message); # convert text parts into utf-8 - - return ($Message); - -} - - - -=head2 ParseDateToISO - -Takes a date in an arbitrary format. -Returns an ISO date and time in GMT - -=cut - -sub ParseDateToISO { - my $date = shift; - - my $date_obj = RT::Date->new( $session{'CurrentUser'} ); - $date_obj->Set( - Format => 'unknown', - Value => $date - ); - return ( $date_obj->ISO ); -} - - - -sub ProcessACLChanges { - my $ARGSref = shift; - - my @results; - - foreach my $arg ( keys %$ARGSref ) { - next unless ( $arg =~ /^(GrantRight|RevokeRight)-(\d+)-(.+?)-(\d+)$/ ); - - my ( $method, $principal_id, $object_type, $object_id ) = ( $1, $2, $3, $4 ); - - my @rights; - if ( UNIVERSAL::isa( $ARGSref->{$arg}, 'ARRAY' ) ) { - @rights = @{ $ARGSref->{$arg} }; - } else { - @rights = $ARGSref->{$arg}; - } - @rights = grep $_, @rights; - next unless @rights; - - 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; - } - - foreach my $right (@rights) { - my ( $val, $msg ) = $principal->$method( Object => $obj, Right => $right ); - push( @results, $msg ); - } - } - - return (@results); -} - - -=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 $principal = _ParseACLNewPrincipal($ARGSref, $type) - or next; - - 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 _ParseACLNewPrincipal - -Takes a hashref of C<%ARGS> and a principal type (C or C). Looks -for the presence of rights being added on a principal of the specified type, -and returns undef if no new principal is being granted rights. Otherwise loads -up an L or L object and returns it. Note that the object -may not be successfully loaded, and you should check C<->id> yourself. - -=cut - -sub _ParseACLNewPrincipal { - my $ARGSref = shift; - my $type = lc shift; - my $key = "AddPrincipalForRights-$type"; - - return unless $ARGSref->{$key}; - - my $principal; - if ( $type eq 'user' ) { - $principal = RT::User->new( $session{'CurrentUser'} ); - $principal->LoadByCol( Name => $ARGSref->{$key} ); - } - elsif ( $type eq 'group' ) { - $principal = RT::Group->new( $session{'CurrentUser'} ); - $principal->LoadUserDefinedGroup( $ARGSref->{$key} ); - } - return $principal; -} - - -=head2 UpdateRecordObj ( ARGSRef => \%ARGS, Object => RT::Record, AttributesRef => \@attribs) - -@attribs is a list of ticket fields to check and update if they differ from the B's current values. ARGSRef is a ref to HTML::Mason's %ARGS. - -Returns an array of success/failure messages - -=cut - -sub UpdateRecordObject { - my %args = ( - ARGSRef => undef, - AttributesRef => undef, - Object => undef, - AttributePrefix => undef, - @_ - ); - - my $Object = $args{'Object'}; - my @results = $Object->Update( - AttributesRef => $args{'AttributesRef'}, - ARGSRef => $args{'ARGSRef'}, - AttributePrefix => $args{'AttributePrefix'}, - ); - - return (@results); -} - - - -sub ProcessCustomFieldUpdates { - my %args = ( - CustomFieldObj => undef, - ARGSRef => undef, - @_ - ); - - my $Object = $args{'CustomFieldObj'}; - my $ARGSRef = $args{'ARGSRef'}; - - my @attribs = qw(Name Type Description Queue SortOrder); - my @results = UpdateRecordObject( - AttributesRef => \@attribs, - Object => $Object, - ARGSRef => $ARGSRef - ); - - my $prefix = "CustomField-" . $Object->Id; - if ( $ARGSRef->{"$prefix-AddValue-Name"} ) { - my ( $addval, $addmsg ) = $Object->AddValue( - Name => $ARGSRef->{"$prefix-AddValue-Name"}, - Description => $ARGSRef->{"$prefix-AddValue-Description"}, - SortOrder => $ARGSRef->{"$prefix-AddValue-SortOrder"}, - ); - push( @results, $addmsg ); - } - - my @delete_values - = ( ref $ARGSRef->{"$prefix-DeleteValue"} eq 'ARRAY' ) - ? @{ $ARGSRef->{"$prefix-DeleteValue"} } - : ( $ARGSRef->{"$prefix-DeleteValue"} ); - - foreach my $id (@delete_values) { - next unless defined $id; - my ( $err, $msg ) = $Object->DeleteValue($id); - push( @results, $msg ); - } - - my $vals = $Object->Values(); - while ( my $cfv = $vals->Next() ) { - if ( my $so = $ARGSRef->{ "$prefix-SortOrder" . $cfv->Id } ) { - if ( $cfv->SortOrder != $so ) { - my ( $err, $msg ) = $cfv->SetSortOrder($so); - push( @results, $msg ); - } - } - } - - return (@results); -} - - - -=head2 ProcessTicketBasics ( TicketObj => $Ticket, ARGSRef => \%ARGS ); - -Returns an array of results messages. - -=cut - -sub ProcessTicketBasics { - - my %args = ( - TicketObj => undef, - ARGSRef => undef, - @_ - ); - - my $TicketObj = $args{'TicketObj'}; - my $ARGSRef = $args{'ARGSRef'}; - - my $OrigOwner = $TicketObj->Owner; - - # Set basic fields - my @attribs = qw( - Subject - FinalPriority - Priority - TimeEstimated - TimeWorked - TimeLeft - Type - Status - Queue - ); - - # 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; - } - } - } - - # Status isn't a field that can be set to a null value. - # RT core complains if you try - delete $ARGSRef->{'Status'} unless $ARGSRef->{'Status'}; - - my @results = UpdateRecordObject( - AttributesRef => \@attribs, - Object => $TicketObj, - ARGSRef => $ARGSRef, - ); - - # We special case owner changing, so we can use ForceOwnerChange - if ( $ARGSRef->{'Owner'} - && $ARGSRef->{'Owner'} !~ /\D/ - && ( $OrigOwner != $ARGSRef->{'Owner'} ) ) { - my ($ChownType); - if ( $ARGSRef->{'ForceOwnerChange'} ) { - $ChownType = "Force"; - } - else { - $ChownType = "Set"; - } - - my ( $val, $msg ) = $TicketObj->SetOwner( $ARGSRef->{'Owner'}, $ChownType ); - push( @results, $msg ); - } - - # }}} - - 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 ) { - my $resolve_status = $reminder->QueueObj->Lifecycle->ReminderStatusOnResolve; - if ( $reminder->Status ne $resolve_status && $args->{ 'Complete-Reminder-' . $reminder->id } ) { - my ($status, $msg) = $Ticket->Reminders->Resolve($reminder); - push @results, loc("Reminder #[_1]: [_2]", $reminder->id, $msg); - - } - elsif ( $reminder->Status eq $resolve_status && !$args->{ 'Complete-Reminder-' . $reminder->id } ) { - my ($status, $msg) = $Ticket->Reminders->Open($reminder); - push @results, loc("Reminder #[_1]: [_2]", $reminder->id, $msg); - } - - if ( exists( $args->{ 'Reminder-Subject-' . $reminder->id } ) && ( $reminder->Subject ne $args->{ 'Reminder-Subject-' . $reminder->id } )) { - my ($status, $msg) = $reminder->SetSubject( $args->{ 'Reminder-Subject-' . $reminder->id } ) ; - push @results, loc("Reminder #[_1]: [_2]", $reminder->id, $msg); - } - - if ( exists( $args->{ 'Reminder-Owner-' . $reminder->id } ) && ( $reminder->Owner != $args->{ 'Reminder-Owner-' . $reminder->id } )) { - my ($status, $msg) = $reminder->SetOwner( $args->{ 'Reminder-Owner-' . $reminder->id } , "Force" ) ; - push @results, loc("Reminder #[_1]: [_2]", $reminder->id, $msg); - } - - 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 ) { - my ($status, $msg) = $reminder->SetDue( $DateObj->ISO ); - push @results, loc("Reminder #[_1]: [_2]", $reminder->id, $msg); - } - } - } - } - - 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 ) = $Ticket->Reminders->Add( - Subject => $args->{'NewReminder-Subject'}, - Owner => $args->{'NewReminder-Owner'}, - Due => $due_obj->ISO - ); - if ( $add_id ) { - push @results, loc("Reminder '[_1]' added", $args->{'NewReminder-Subject'}); - } - else { - push @results, $msg; - } - } - return @results; -} - -sub ProcessTicketCustomFieldUpdates { - my %args = @_; - $args{'Object'} = delete $args{'TicketObj'}; - my $ARGSRef = { %{ $args{'ARGSRef'} } }; - - # Build up a list of objects that we want to work with - my %custom_fields_to_mod; - foreach my $arg ( keys %$ARGSRef ) { - if ( $arg =~ /^Ticket-(\d+-.*)/ ) { - $ARGSRef->{"Object-RT::Ticket-$1"} = delete $ARGSRef->{$arg}; - } elsif ( $arg =~ /^CustomField-(\d+-.*)/ ) { - $ARGSRef->{"Object-RT::Ticket--$1"} = delete $ARGSRef->{$arg}; - } elsif ( $arg =~ /^Object-RT::Transaction-(\d*)-CustomField/ ) { - delete $ARGSRef->{$arg}; # don't try to update transaction fields - } - } - - return ProcessObjectCustomFieldUpdates( %args, ARGSRef => $ARGSRef ); -} - -sub ProcessObjectCustomFieldUpdates { - my %args = @_; - my $ARGSRef = $args{'ARGSRef'}; - my @results; - - # Build up a list of objects that we want to work with - my %custom_fields_to_mod; - foreach my $arg ( keys %$ARGSRef ) { - - # format: Object---CustomField-- - next unless $arg =~ /^Object-([\w:]+)-(\d*)-CustomField-(\d+)-(.*)$/; - - # For each of those objects, find out what custom fields we want to work with. - $custom_fields_to_mod{$1}{ $2 || 0 }{$3}{$4} = $ARGSRef->{$arg}; - } - - # For each of those objects - foreach my $class ( keys %custom_fields_to_mod ) { - foreach my $id ( keys %{ $custom_fields_to_mod{$class} } ) { - my $Object = $args{'Object'}; - $Object = $class->new( $session{'CurrentUser'} ) - unless $Object && ref $Object eq $class; - - $Object->Load($id) unless ( $Object->id || 0 ) == $id; - unless ( $Object->id ) { - $RT::Logger->warning("Couldn't load object $class #$id"); - next; - } - - foreach my $cf ( keys %{ $custom_fields_to_mod{$class}{$id} } ) { - my $CustomFieldObj = RT::CustomField->new( $session{'CurrentUser'} ); - $CustomFieldObj->SetContextObject($Object); - $CustomFieldObj->LoadById($cf); - unless ( $CustomFieldObj->id ) { - $RT::Logger->warning("Couldn't load custom field #$cf"); - next; - } - push @results, - _ProcessObjectCustomFieldUpdates( - Prefix => "Object-$class-$id-CustomField-$cf-", - Object => $Object, - CustomField => $CustomFieldObj, - ARGS => $custom_fields_to_mod{$class}{$id}{$cf}, - ); - } - } - } - return @results; -} - -sub _ProcessObjectCustomFieldUpdates { - my %args = @_; - my $cf = $args{'CustomField'}; - 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 - if ( defined $args{'ARGS'}->{'Values'} - && !length $args{'ARGS'}->{'Values'} - && $args{'ARGS'}->{'Values-Magic'} ) - { - delete $args{'ARGS'}->{'Values'}; - } - - my @results; - foreach my $arg ( keys %{ $args{'ARGS'} } ) { - - # skip category argument - next if $arg eq 'Category'; - - # and TimeUnits - next if $arg eq 'Value-TimeUnits'; - - # since http won't pass in a form element with a null value, we need - # to fake it - if ( $arg eq 'Values-Magic' ) { - - # We don't care about the magic, if there's really a values element; - next if defined $args{'ARGS'}->{'Value'} && length $args{'ARGS'}->{'Value'}; - next if defined $args{'ARGS'}->{'Values'} && length $args{'ARGS'}->{'Values'}; - - # "Empty" values does not mean anything for Image and Binary fields - next if $cf_type =~ /^(?:Image|Binary)$/; - - $arg = 'Values'; - $args{'ARGS'}->{'Values'} = undef; - } - - my @values = (); - if ( ref $args{'ARGS'}->{$arg} eq 'ARRAY' ) { - @values = @{ $args{'ARGS'}->{$arg} }; - } elsif ( $cf_type =~ /text/i ) { # Both Text and Wikitext - @values = ( $args{'ARGS'}->{$arg} ); - } else { - @values = split /\r*\n/, $args{'ARGS'}->{$arg} - if defined $args{'ARGS'}->{$arg}; - } - @values = grep length, map { - s/\r+\n/\n/g; - s/^\s+//; - s/\s+$//; - $_; - } - grep defined, @values; - - if ( $arg eq 'AddValue' || $arg eq 'Value' ) { - foreach my $value (@values) { - my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue( - Field => $cf->id, - Value => $value - ); - push( @results, $msg ); - } - } elsif ( $arg eq 'Upload' ) { - my $value_hash = _UploadedFile( $args{'Prefix'} . $arg ) or next; - my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue( %$value_hash, Field => $cf, ); - push( @results, $msg ); - } elsif ( $arg eq 'DeleteValues' ) { - foreach my $value (@values) { - my ( $val, $msg ) = $args{'Object'}->DeleteCustomFieldValue( - Field => $cf, - Value => $value, - ); - push( @results, $msg ); - } - } elsif ( $arg eq 'DeleteValueIds' ) { - foreach my $value (@values) { - my ( $val, $msg ) = $args{'Object'}->DeleteCustomFieldValue( - Field => $cf, - ValueId => $value, - ); - push( @results, $msg ); - } - } elsif ( $arg eq 'Values' && !$cf->Repeated ) { - my $cf_values = $args{'Object'}->CustomFieldValues( $cf->id ); - - my %values_hash; - foreach my $value (@values) { - if ( my $entry = $cf_values->HasEntry($value) ) { - $values_hash{ $entry->id } = 1; - next; - } - - my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue( - Field => $cf, - Value => $value - ); - push( @results, $msg ); - $values_hash{$val} = 1 if $val; - } - - # 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 }; - - my ( $val, $msg ) = $args{'Object'}->DeleteCustomFieldValue( - Field => $cf, - ValueId => $cf_value->id - ); - push( @results, $msg ); - } - } elsif ( $arg eq 'Values' ) { - my $cf_values = $args{'Object'}->CustomFieldValues( $cf->id ); - - # keep everything up to the point of difference, delete the rest - my $delete_flag; - foreach my $old_cf ( @{ $cf_values->ItemsArrayRef } ) { - if ( !$delete_flag and @values and $old_cf->Content eq $values[0] ) { - shift @values; - next; - } - - $delete_flag ||= 1; - $old_cf->Delete; - } - - # now add/replace extra things, if any - foreach my $value (@values) { - my ( $val, $msg ) = $args{'Object'}->AddCustomFieldValue( - Field => $cf, - Value => $value - ); - push( @results, $msg ); - } - } else { - push( - @results, - loc("User asked for an unknown update type for custom field [_1] for [_2] object #[_3]", - $cf->Name, ref $args{'Object'}, - $args{'Object'}->id - ) - ); - } - } - return @results; -} - - -=head2 ProcessTicketWatchers ( TicketObj => $Ticket, ARGSRef => \%ARGS ); - -Returns an array of results messages. - -=cut - -sub ProcessTicketWatchers { - my %args = ( - TicketObj => undef, - ARGSRef => undef, - @_ - ); - my (@results); - - my $Ticket = $args{'TicketObj'}; - my $ARGSRef = $args{'ARGSRef'}; - - # Munge watchers - - foreach my $key ( keys %$ARGSRef ) { - - # Delete deletable watchers - if ( $key =~ /^Ticket-DeleteWatcher-Type-(.*)-Principal-(\d+)$/ ) { - my ( $code, $msg ) = $Ticket->DeleteWatcher( - PrincipalId => $2, - Type => $1 - ); - push @results, $msg; - } - - # Delete watchers in the simple style demanded by the bulk manipulator - elsif ( $key =~ /^Delete(Requestor|Cc|AdminCc)$/ ) { - my ( $code, $msg ) = $Ticket->DeleteWatcher( - Email => $ARGSRef->{$key}, - Type => $1 - ); - push @results, $msg; - } - - # Add new wathchers by email address - elsif ( ( $ARGSRef->{$key} || '' ) =~ /^(?:AdminCc|Cc|Requestor)$/ - and $key =~ /^WatcherTypeEmail(\d*)$/ ) - { - - #They're in this order because otherwise $1 gets clobbered :/ - my ( $code, $msg ) = $Ticket->AddWatcher( - Type => $ARGSRef->{$key}, - Email => $ARGSRef->{ "WatcherAddressEmail" . $1 } - ); - push @results, $msg; - } - - #Add requestors in the simple style demanded by the bulk manipulator - elsif ( $key =~ /^Add(Requestor|Cc|AdminCc)$/ ) { - my ( $code, $msg ) = $Ticket->AddWatcher( - Type => $1, - Email => $ARGSRef->{$key} - ); - push @results, $msg; - } - - # Add new watchers by owner - elsif ( $key =~ /^Ticket-AddWatcher-Principal-(\d*)$/ ) { - my $principal_id = $1; - my $form = $ARGSRef->{$key}; - foreach my $value ( ref($form) ? @{$form} : ($form) ) { - next unless $value =~ /^(?:AdminCc|Cc|Requestor)$/i; - - my ( $code, $msg ) = $Ticket->AddWatcher( - Type => $value, - PrincipalId => $principal_id - ); - push @results, $msg; - } - } - - } - return (@results); -} - - - -=head2 ProcessTicketDates ( TicketObj => $Ticket, ARGSRef => \%ARGS ); - -Returns an array of results messages. - -=cut - -sub ProcessTicketDates { - my %args = ( - TicketObj => undef, - ARGSRef => undef, - @_ - ); - - my $Ticket = $args{'TicketObj'}; - my $ARGSRef = $args{'ARGSRef'}; - - my (@results); - - # Set date fields - my @date_fields = qw( - Told - Resolved - Starts - Started - Due - WillResolve - ); - - #Run through each field in this list. update the value if apropriate - foreach my $field (@date_fields) { - next unless exists $ARGSRef->{ $field . '_Date' }; - next if $ARGSRef->{ $field . '_Date' } eq ''; - - my ( $code, $msg ); - - my $DateObj = RT::Date->new( $session{'CurrentUser'} ); - $DateObj->Set( - Format => 'unknown', - Value => $ARGSRef->{ $field . '_Date' } - ); - - my $obj = $field . "Obj"; - if ( ( defined $DateObj->Unix ) - and ( $DateObj->Unix != $Ticket->$obj()->Unix() ) ) - { - my $method = "Set$field"; - my ( $code, $msg ) = $Ticket->$method( $DateObj->ISO ); - push @results, "$msg"; - } - } - - # }}} - return (@results); -} - - - -=head2 ProcessTicketLinks ( TicketObj => $Ticket, ARGSRef => \%ARGS ); - -Returns an array of results messages. - -=cut - -sub ProcessTicketLinks { - my %args = ( - TicketObj => undef, - ARGSRef => undef, - @_ - ); - - my $Ticket = $args{'TicketObj'}; - my $ARGSRef = $args{'ARGSRef'}; - - my (@results) = ProcessRecordLinks( RecordObj => $Ticket, ARGSRef => $ARGSRef ); - - #Merge if we need to - if ( $ARGSRef->{ $Ticket->Id . "-MergeInto" } ) { - $ARGSRef->{ $Ticket->Id . "-MergeInto" } =~ s/\s+//g; - my ( $val, $msg ) = $Ticket->MergeInto( $ARGSRef->{ $Ticket->Id . "-MergeInto" } ); - push @results, $msg; - } - - return (@results); -} - - -sub ProcessRecordLinks { - my %args = ( - RecordObj => undef, - ARGSRef => undef, - @_ - ); - - my $Record = $args{'RecordObj'}; - my $ARGSRef = $args{'ARGSRef'}; - - my (@results); - - # Delete links that are gone gone gone. - foreach my $arg ( keys %$ARGSRef ) { - if ( $arg =~ /DeleteLink-(.*?)-(DependsOn|MemberOf|RefersTo)-(.*)$/ ) { - my $base = $1; - my $type = $2; - my $target = $3; - - my ( $val, $msg ) = $Record->DeleteLink( - Base => $base, - Type => $type, - Target => $target - ); - - push @results, $msg; - - } - - } - - my @linktypes = qw( DependsOn MemberOf RefersTo ); - - foreach my $linktype (@linktypes) { - if ( $ARGSRef->{ $Record->Id . "-$linktype" } ) { - $ARGSRef->{ $Record->Id . "-$linktype" } = join( ' ', @{ $ARGSRef->{ $Record->Id . "-$linktype" } } ) - if ref( $ARGSRef->{ $Record->Id . "-$linktype" } ); - - for my $luri ( split( / /, $ARGSRef->{ $Record->Id . "-$linktype" } ) ) { - next unless $luri; - $luri =~ s/\s+$//; # Strip trailing whitespace - my ( $val, $msg ) = $Record->AddLink( - Target => $luri, - Type => $linktype - ); - push @results, $msg; - } - } - if ( $ARGSRef->{ "$linktype-" . $Record->Id } ) { - $ARGSRef->{ "$linktype-" . $Record->Id } = join( ' ', @{ $ARGSRef->{ "$linktype-" . $Record->Id } } ) - if ref( $ARGSRef->{ "$linktype-" . $Record->Id } ); - - for my $luri ( split( / /, $ARGSRef->{ "$linktype-" . $Record->Id } ) ) { - next unless $luri; - my ( $val, $msg ) = $Record->AddLink( - Base => $luri, - Type => $linktype - ); - - push @results, $msg; - } - } - } - - return (@results); -} - -=head2 ProcessTransactionSquelching - -Takes a hashref of the submitted form arguments, C<%ARGS>. - -Returns a hash of squelched addresses. - -=cut - -sub ProcessTransactionSquelching { - my $args = shift; - my %checked = map { $_ => 1 } grep { defined } - ( ref $args->{'TxnSendMailTo'} eq "ARRAY" ? @{$args->{'TxnSendMailTo'}} : - defined $args->{'TxnSendMailTo'} ? ($args->{'TxnSendMailTo'}) : - () ); - my %squelched = map { $_ => 1 } grep { not $checked{$_} } split /,/, ($args->{'TxnRecipients'}||''); - return %squelched; -} - -=head2 _UploadedFile ( $arg ); - -Takes a CGI parameter name; if a file is uploaded under that name, -return a hash reference suitable for AddCustomFieldValue's use: -C<( Value => $filename, LargeContent => $content, ContentType => $type )>. - -Returns C if no files were uploaded in the C<$arg> field. - -=cut - -sub _UploadedFile { - my $arg = shift; - my $cgi_object = $m->cgi_object; - my $fh = $cgi_object->upload($arg) or return undef; - my $upload_info = $cgi_object->uploadInfo($fh); - - my $filename = "$fh"; - $filename =~ s#^.*[\\/]##; - binmode($fh); - - return { - Value => $filename, - LargeContent => do { local $/; scalar <$fh> }, - ContentType => $upload_info->{'Content-Type'}, - }; -} - -sub GetColumnMapEntry { - my %args = ( Map => {}, Name => '', Attribute => undef, @_ ); - - # deal with the simplest thing first - if ( $args{'Map'}{ $args{'Name'} } ) { - return $args{'Map'}{ $args{'Name'} }{ $args{'Attribute'} }; - } - - # complex things - elsif ( my ( $mainkey, $subkey ) = $args{'Name'} =~ /^(.*?)\.(.+)$/ ) { - $subkey =~ s/^\{(.*)\}$/$1/; - return undef unless $args{'Map'}->{$mainkey}; - return $args{'Map'}{$mainkey}{ $args{'Attribute'} } - unless ref $args{'Map'}{$mainkey}{ $args{'Attribute'} } eq 'CODE'; - - return sub { $args{'Map'}{$mainkey}{ $args{'Attribute'} }->( @_, $subkey ) }; - } - return undef; -} - -sub ProcessColumnMapValue { - my $value = shift; - my %args = ( Arguments => [], Escape => 1, @_ ); - - if ( ref $value ) { - if ( UNIVERSAL::isa( $value, 'CODE' ) ) { - my @tmp = $value->( @{ $args{'Arguments'} } ); - return ProcessColumnMapValue( ( @tmp > 1 ? \@tmp : $tmp[0] ), %args ); - } elsif ( UNIVERSAL::isa( $value, 'ARRAY' ) ) { - return join '', map ProcessColumnMapValue( $_, %args ), @$value; - } elsif ( UNIVERSAL::isa( $value, 'SCALAR' ) ) { - return $$value; - } - } - - return $m->interp->apply_escapes( $value, 'h' ) if $args{'Escape'}; - 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. - -=cut - -sub _load_container_object { - my ( $obj_type, $obj_id ) = @_; - return RT::SavedSearch->new( $session{'CurrentUser'} )->_load_privacy_object( $obj_type, $obj_id ); -} - -=head2 _parse_saved_search ( $arg ); - -Given a serialization string for saved search, and returns the -container object and the search id. - -=cut - -sub _parse_saved_search { - my $spec = shift; - return unless $spec; - if ( $spec !~ /^(.*?)-(\d+)-SavedSearch-(\d+)$/ ) { - return; - } - my $obj_type = $1; - my $obj_id = $2; - my $search_id = $3; - - 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 object. - -If you need to be more lax about what HTML tags and attributes are allowed, -create C with something like the -following: - - package HTML::Mason::Commands; - # Let tables through - push @SCRUBBER_ALLOWED_TAGS, qw(TABLE THEAD TBODY TFOOT TR TD TH); - 1; - -=cut - -our @SCRUBBER_ALLOWED_TAGS = 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 BDO -); - -our %SCRUBBER_ALLOWED_ATTRIBUTES = ( - # Match http, https, ftp, mailto and relative urls - # XXX: we also scrub format strings with this module then allow simple config options - href => qr{^(?:https?:|ftp:|mailto:|/|__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, - dir => qr/^(rtl|ltr)$/i, - lang => qr/^\w+(-\w+)?$/, -); - -our %SCRUBBER_RULES = (); - -sub _NewScrubber { - require HTML::Scrubber; - my $scrubber = HTML::Scrubber->new(); - $scrubber->default( - 0, - { - %SCRUBBER_ALLOWED_ATTRIBUTES, - '*' => 0, # require attributes be explicitly allowed - }, - ); - $scrubber->deny(qw[*]); - $scrubber->allow(@SCRUBBER_ALLOWED_TAGS); - $scrubber->rules(%SCRUBBER_RULES); - - # Scrubbing comments is vital since IE conditional comments can contain - # arbitrary HTML and we'd pass it right on through. - $scrubber->comment(0); - - return $scrubber; -} - -=head2 JSON - -Redispatches to L - -=cut - -sub JSON { - RT::Interface::Web::EncodeJSON(@_); -} - -package RT::Interface::Web; -RT::Base->_ImportOverlays(); - -1;