summaryrefslogtreecommitdiff
path: root/rt/lib
diff options
context:
space:
mode:
authorIvan Kohler <ivan@freeside.biz>2012-12-12 10:26:29 -0800
committerIvan Kohler <ivan@freeside.biz>2012-12-12 10:26:29 -0800
commit84f2df8931fa02e63fb21f8f0bb87dd9577b8919 (patch)
tree8cbe43418fe23f3fc28cc8f3e19a61666a1696ed /rt/lib
parentd7678b9b97068dcd352f0ea101c6c8d02ae330d6 (diff)
rt 4.0.8
Diffstat (limited to 'rt/lib')
-rwxr-xr-xrt/lib/RT/Action/SendEmail.pm54
-rw-r--r--rt/lib/RT/Approval/Rule/Passed.pm11
-rw-r--r--rt/lib/RT/Article.pm2
-rwxr-xr-xrt/lib/RT/Attachment.pm49
-rw-r--r--rt/lib/RT/Crypt/GnuPG.pm27
-rw-r--r--rt/lib/RT/Generated.pm2
-rw-r--r--rt/lib/RT/Handle.pm30
-rwxr-xr-xrt/lib/RT/Interface/Email.pm52
-rwxr-xr-xrt/lib/RT/Interface/Email/Auth/GnuPG.pm3
-rw-r--r--rt/lib/RT/Interface/Web.pm117
-rw-r--r--rt/lib/RT/Interface/Web/Menu.pm11
-rw-r--r--rt/lib/RT/Pod/HTML.pm66
-rw-r--r--rt/lib/RT/Pod/HTMLBatch.pm131
-rw-r--r--rt/lib/RT/Pod/Search.pm15
-rwxr-xr-xrt/lib/RT/Queue.pm40
-rwxr-xr-xrt/lib/RT/Record.pm35
-rwxr-xr-xrt/lib/RT/Template.pm1
-rwxr-xr-xrt/lib/RT/Ticket.pm10
-rwxr-xr-xrt/lib/RT/User.pm1
19 files changed, 534 insertions, 123 deletions
diff --git a/rt/lib/RT/Action/SendEmail.pm b/rt/lib/RT/Action/SendEmail.pm
index 4ae1a8b..2a7a2e3 100755
--- a/rt/lib/RT/Action/SendEmail.pm
+++ b/rt/lib/RT/Action/SendEmail.pm
@@ -99,47 +99,31 @@ activated in the config.
sub Commit {
my $self = shift;
- $self->DeferDigestRecipients() if RT->Config->Get('RecordOutgoingEmail');
+ return abs $self->SendMessage( $self->TemplateObj->MIMEObj )
+ unless RT->Config->Get('RecordOutgoingEmail');
+
+ $self->DeferDigestRecipients();
my $message = $self->TemplateObj->MIMEObj;
my $orig_message;
- if ( RT->Config->Get('RecordOutgoingEmail')
- && RT->Config->Get('GnuPG')->{'Enable'} )
- {
-
- # it's hacky, but we should know if we're going to crypt things
- my $attachment = $self->TransactionObj->Attachments->First;
-
- my %crypt;
- foreach my $argument (qw(Sign Encrypt)) {
- if ( $attachment
- && defined $attachment->GetHeader("X-RT-$argument") )
- {
- $crypt{$argument} = $attachment->GetHeader("X-RT-$argument");
- } else {
- $crypt{$argument} = $self->TicketObj->QueueObj->$argument();
- }
- }
- if ( $crypt{'Sign'} || $crypt{'Encrypt'} ) {
- $orig_message = $message->dup;
- }
- }
+ $orig_message = $message->dup if RT::Interface::Email::WillSignEncrypt(
+ Attachment => $self->TransactionObj->Attachments->First,
+ Ticket => $self->TicketObj,
+ );
my ($ret) = $self->SendMessage($message);
- if ( $ret > 0 && RT->Config->Get('RecordOutgoingEmail') ) {
- if ($orig_message) {
- $message->attach(
- Type => 'application/x-rt-original-message',
- Disposition => 'inline',
- Data => $orig_message->as_string,
- );
- }
- $self->RecordOutgoingMailTransaction($message);
- $self->RecordDeferredRecipients();
- }
-
+ return abs( $ret ) if $ret <= 0;
- return ( abs $ret );
+ 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
diff --git a/rt/lib/RT/Approval/Rule/Passed.pm b/rt/lib/RT/Approval/Rule/Passed.pm
index f364bc9..000a8dc 100644
--- a/rt/lib/RT/Approval/Rule/Passed.pm
+++ b/rt/lib/RT/Approval/Rule/Passed.pm
@@ -80,10 +80,8 @@ sub Commit {
}
}
- $obj->SetStatus(
- Status => $obj->QueueObj->Lifecycle->DefaultStatus('approved') || 'open',
- Force => 1,
- );
+ $obj->SetStatus( Status => $obj->FirstActiveStatus, Force => 1 )
+ if $obj->FirstActiveStatus;
}
my $passed = !$top->HasUnresolvedDependencies( Type => 'approval' );
@@ -98,6 +96,11 @@ sub Commit {
$top->Correspond( MIMEObj => $template->MIMEObj );
if ($passed) {
+ my $new_status = $top->QueueObj->Lifecycle->DefaultStatus('approved') || 'open';
+ if ( $new_status ne $top->Status ) {
+ $top->SetStatus( $new_status );
+ }
+
$self->RunScripAction('Notify Owner', 'Approval Ready for Owner',
TicketObj => $top);
}
diff --git a/rt/lib/RT/Article.pm b/rt/lib/RT/Article.pm
index 24b952a..678aa11 100644
--- a/rt/lib/RT/Article.pm
+++ b/rt/lib/RT/Article.pm
@@ -102,7 +102,7 @@ sub Create {
@_
);
- my $class = RT::Class->new($RT::SystemUser);
+ my $class = RT::Class->new( $self->CurrentUser );
$class->Load( $args{'Class'} );
unless ( $class->Id ) {
return ( 0, $self->loc('Invalid Class') );
diff --git a/rt/lib/RT/Attachment.pm b/rt/lib/RT/Attachment.pm
index fb17da3..f1d9a63 100755
--- a/rt/lib/RT/Attachment.pm
+++ b/rt/lib/RT/Attachment.pm
@@ -600,8 +600,8 @@ sub DelHeader {
my $newheader = '';
foreach my $line ($self->_SplitHeaders) {
- next if $line =~ /^\Q$tag\E:\s+(.*)$/is;
- $newheader .= "$line\n";
+ next if $line =~ /^\Q$tag\E:\s+/i;
+ $newheader .= "$line\n";
}
return $self->__Set( Field => 'Headers', Value => $newheader);
}
@@ -617,9 +617,7 @@ sub AddHeader {
my $newheader = $self->__Value( 'Headers' );
while ( my ($tag, $value) = splice @_, 0, 2 ) {
- $value = '' unless defined $value;
- $value =~ s/\s+$//s;
- $value =~ s/\r+\n/\n /g;
+ $value = $self->_CanonicalizeHeaderValue($value);
$newheader .= "$tag: $value\n";
}
return $self->__Set( Field => 'Headers', Value => $newheader);
@@ -632,24 +630,39 @@ Replace or add a Header to the attachment's headers.
=cut
sub SetHeader {
- my $self = shift;
- my $tag = shift;
+ my $self = shift;
+ my $tag = shift;
+ my $value = $self->_CanonicalizeHeaderValue(shift);
+ my $replaced = 0;
my $newheader = '';
- foreach my $line ($self->_SplitHeaders) {
- if (defined $tag and $line =~ /^\Q$tag\E:\s+(.*)$/i) {
- $newheader .= "$tag: $_[0]\n";
- undef $tag;
+ foreach my $line ( $self->_SplitHeaders ) {
+ if ( $line =~ /^\Q$tag\E:\s+/i ) {
+ # replace first instance, skip all the rest
+ unless ($replaced) {
+ $newheader .= "$tag: $value\n";
+ $replaced = 1;
+ }
+ } else {
+ $newheader .= "$line\n";
}
- else {
- $newheader .= "$line\n";
- }
}
- $newheader .= "$tag: $_[0]\n" if defined $tag;
+ $newheader .= "$tag: $value\n" unless $replaced;
$self->__Set( Field => 'Headers', Value => $newheader);
}
+sub _CanonicalizeHeaderValue {
+ my $self = shift;
+ my $value = shift;
+
+ $value = '' unless defined $value;
+ $value =~ s/\s+$//s;
+ $value =~ s/\r*\n/\n /g;
+
+ return $value;
+}
+
=head2 SplitHeaders
Returns an array of this attachment object's headers, with one header
@@ -676,6 +689,12 @@ sub _SplitHeaders {
my $self = shift;
my $headers = (shift || $self->_Value('Headers'));
my @headers;
+ # XXX TODO: splitting on \n\w is _wrong_ as it treats \n[ as a valid
+ # continuation, which it isn't. The correct split pattern, per RFC 2822,
+ # is /\n(?=[^ \t]|\z)/. That is, only "\n " or "\n\t" is a valid
+ # continuation. Older values of X-RT-GnuPG-Status contain invalid
+ # continuations and rely on this bogus split pattern, however, so it is
+ # left as-is for now.
for (split(/\n(?=\w|\z)/,$headers)) {
push @headers, $_;
diff --git a/rt/lib/RT/Crypt/GnuPG.pm b/rt/lib/RT/Crypt/GnuPG.pm
index c5fb12b..2330478 100644
--- a/rt/lib/RT/Crypt/GnuPG.pm
+++ b/rt/lib/RT/Crypt/GnuPG.pm
@@ -900,6 +900,19 @@ sub FindProtectedParts {
$RT::Logger->warning( "Entity of type ". $entity->effective_type ." has no body" );
return ();
}
+
+ # Deal with "partitioned" PGP mail, which (contrary to common
+ # sense) unnecessarily applies a base64 transfer encoding to PGP
+ # mail (whose content is already base64-encoded).
+ if ( $entity->bodyhandle->is_encoded and $entity->head->mime_encoding ) {
+ pipe( my ($read_decoded, $write_decoded) );
+ my $decoder = MIME::Decoder->new( $entity->head->mime_encoding );
+ if ($decoder) {
+ eval { $decoder->decode($io, $write_decoded) };
+ $io = $read_decoded;
+ }
+ }
+
while ( defined($_ = $io->getline) ) {
next unless /^-----BEGIN PGP (SIGNED )?MESSAGE-----/;
my $type = $1? 'signed': 'encrypted';
@@ -1064,9 +1077,13 @@ sub VerifyDecrypt {
}
if ( $args{'SetStatus'} || $args{'AddStatus'} ) {
my $method = $args{'AddStatus'} ? 'add' : 'set';
+ # Let the header be modified so continuations are handled
+ my $modify = $status_on->head->modify;
+ $status_on->head->modify(1);
$status_on->head->$method(
'X-RT-GnuPG-Status' => $res[-1]->{'status'}
);
+ $status_on->head->modify($modify);
}
}
foreach my $item( grep $_->{'Type'} eq 'encrypted', @protected ) {
@@ -1083,9 +1100,13 @@ sub VerifyDecrypt {
}
if ( $args{'SetStatus'} || $args{'AddStatus'} ) {
my $method = $args{'AddStatus'} ? 'add' : 'set';
+ # Let the header be modified so continuations are handled
+ my $modify = $status_on->head->modify;
+ $status_on->head->modify(1);
$status_on->head->$method(
'X-RT-GnuPG-Status' => $res[-1]->{'status'}
);
+ $status_on->head->modify($modify);
}
}
return @res;
@@ -2107,7 +2128,9 @@ sub GetKeysInfo {
eval {
local $SIG{'CHLD'} = 'DEFAULT';
my $method = $type eq 'private'? 'list_secret_keys': 'list_public_keys';
- my $pid = safe_run_child { $gnupg->$method( handles => $handles, $email? (command_args => $email) : () ) };
+ my $pid = safe_run_child { $gnupg->$method( handles => $handles, $email
+ ? (command_args => [ "--", $email])
+ : () ) };
close $handle{'stdin'};
waitpid $pid, 0;
};
@@ -2301,7 +2324,7 @@ sub DeleteKey {
my $pid = safe_run_child { $gnupg->wrap_call(
handles => $handles,
commands => ['--delete-secret-and-public-key'],
- command_args => [$key],
+ command_args => ["--", $key],
) };
close $handle{'stdin'};
while ( my $str = readline $handle{'status'} ) {
diff --git a/rt/lib/RT/Generated.pm b/rt/lib/RT/Generated.pm
index 9fd946f..907ea77 100644
--- a/rt/lib/RT/Generated.pm
+++ b/rt/lib/RT/Generated.pm
@@ -50,7 +50,7 @@ package RT;
use warnings;
use strict;
-our $VERSION = '4.0.7';
+our $VERSION = '4.0.8';
diff --git a/rt/lib/RT/Handle.pm b/rt/lib/RT/Handle.pm
index 99d10e3..03c262b 100644
--- a/rt/lib/RT/Handle.pm
+++ b/rt/lib/RT/Handle.pm
@@ -858,26 +858,28 @@ sub InsertData {
@queues = @{ delete $item->{'Queue'} };
}
- my ( $return, $msg ) = $new_entry->Create(%$item);
- unless( $return ) {
- $RT::Logger->error( $msg );
- next;
- }
-
if ( $item->{'BasedOn'} ) {
- my $basedon = RT::CustomField->new($RT::SystemUser);
- my ($ok, $msg ) = $basedon->LoadByCols( Name => $item->{'BasedOn'},
- LookupType => $new_entry->LookupType );
- if ($ok) {
- ($ok, $msg) = $new_entry->SetBasedOn( $basedon );
+ if ( $item->{'LookupType'} ) {
+ my $basedon = RT::CustomField->new($RT::SystemUser);
+ my ($ok, $msg ) = $basedon->LoadByCols( Name => $item->{'BasedOn'},
+ LookupType => $item->{'LookupType'} );
if ($ok) {
- $RT::Logger->debug("Added BasedOn $item->{BasedOn}: $msg");
+ $item->{'BasedOn'} = $basedon->Id;
} else {
- $RT::Logger->error("Failed to add basedOn $item->{BasedOn}: $msg");
+ $RT::Logger->error("Unable to load $item->{BasedOn} as a $item->{LookupType} CF. Skipping BasedOn: $msg");
+ delete $item->{'BasedOn'};
}
} else {
- $RT::Logger->error("Unable to load $item->{BasedOn} as a $item->{LookupType} CF. Skipping BasedOn");
+ $RT::Logger->error("Unable to load CF $item->{BasedOn} because no LookupType was specified. Skipping BasedOn");
+ delete $item->{'BasedOn'};
}
+
+ }
+
+ my ( $return, $msg ) = $new_entry->Create(%$item);
+ unless( $return ) {
+ $RT::Logger->error( $msg );
+ next;
}
foreach my $value ( @{$values} ) {
diff --git a/rt/lib/RT/Interface/Email.pm b/rt/lib/RT/Interface/Email.pm
index 4c3ee99..dda6f70 100755
--- a/rt/lib/RT/Interface/Email.pm
+++ b/rt/lib/RT/Interface/Email.pm
@@ -318,6 +318,35 @@ header field then it's value is 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,
@@ -366,23 +395,12 @@ sub SendEmail {
}
if ( RT->Config->Get('GnuPG')->{'Enable'} ) {
- my %crypt;
-
- my $attachment;
- $attachment = $TransactionObj->Attachments->First
- if $TransactionObj;
-
- foreach my $argument ( qw(Sign Encrypt) ) {
- next if defined $args{ $argument };
-
- if ( $attachment && defined $attachment->GetHeader("X-RT-$argument") ) {
- $crypt{$argument} = $attachment->GetHeader("X-RT-$argument");
- } elsif ( $TicketObj ) {
- $crypt{$argument} = $TicketObj->QueueObj->$argument();
- }
- }
-
- my $res = SignEncrypt( %args, %crypt );
+ %args = WillSignEncrypt(
+ %args,
+ Attachment => $TransactionObj ? $TransactionObj->Attachments->First : undef,
+ Ticket => $TicketObj,
+ );
+ my $res = SignEncrypt( %args );
return $res unless $res > 0;
}
diff --git a/rt/lib/RT/Interface/Email/Auth/GnuPG.pm b/rt/lib/RT/Interface/Email/Auth/GnuPG.pm
index e508908..87a523d 100755
--- a/rt/lib/RT/Interface/Email/Auth/GnuPG.pm
+++ b/rt/lib/RT/Interface/Email/Auth/GnuPG.pm
@@ -77,8 +77,9 @@ sub GetCurrentUser {
foreach my $p ( $args{'Message'}->parts_DFS ) {
$p->head->delete($_) for qw(
- X-RT-GnuPG-Status X-RT-Incoming-Encrypton
+ X-RT-GnuPG-Status X-RT-Incoming-Encryption
X-RT-Incoming-Signature X-RT-Privacy
+ X-RT-Sign X-RT-Encrypt
);
}
diff --git a/rt/lib/RT/Interface/Web.pm b/rt/lib/RT/Interface/Web.pm
index 1aae758..745a6f1 100644
--- a/rt/lib/RT/Interface/Web.pm
+++ b/rt/lib/RT/Interface/Web.pm
@@ -304,12 +304,12 @@ sub HandleRequest {
}
# Specially handle /index.html so that we get a nicer URL
elsif ( $m->request_comp->path eq '/index.html' ) {
- my $next = SetNextPage(RT->Config->Get('WebURL'));
+ my $next = SetNextPage($ARGS);
$m->comp('/NoAuth/Login.html', next => $next, actions => [$msg]);
$m->abort;
}
else {
- TangentForLogin(results => ($msg ? LoginError($msg) : undef));
+ TangentForLogin($ARGS, results => ($msg ? LoginError($msg) : undef));
}
}
}
@@ -364,7 +364,7 @@ sub LoginError {
return $key;
}
-=head2 SetNextPage [PATH]
+=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<IntuitNextPage()>. Returns
@@ -373,24 +373,68 @@ the hash value.
=cut
sub SetNextPage {
- my $next = shift || IntuitNextPage();
+ 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} = $next;
+ $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 [HASH]
+=head2 TangentForLogin ARGSRef [HASH]
Redirects to C</NoAuth/Login.html>, setting the value of L<IntuitNextPage> as
-the next page. Optionally takes a hash which is dumped into query params.
+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 $hash = SetNextPage();
+ my $ARGS = shift;
+ my $hash = SetNextPage($ARGS);
my %query = (@_, next => $hash);
my $login = RT->Config->Get('WebURL') . 'NoAuth/Login.html?';
$login .= $HTML::Mason::Commands::m->comp('/Elements/QueryString', %query);
@@ -405,8 +449,9 @@ calls L<TangentForLogin> with the appropriate results key.
=cut
sub TangentForLoginWithError {
- my $key = LoginError(HTML::Mason::Commands::loc(@_));
- TangentForLogin( results => $key );
+ my $ARGS = shift;
+ my $key = LoginError(HTML::Mason::Commands::loc(@_));
+ TangentForLogin( $ARGS, results => $key );
}
=head2 IntuitNextPage
@@ -606,7 +651,8 @@ sub AttemptExternalAuth {
$user =~ s/^\Q$NodeName\E\\//i;
}
- my $next = delete $HTML::Mason::Commands::session{'NextPage'}->{$ARGS->{'next'} || ''};
+ 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);
@@ -645,7 +691,7 @@ sub AttemptExternalAuth {
delete $HTML::Mason::Commands::session{'CurrentUser'};
if (RT->Config->Get('WebFallbackToInternalAuth')) {
- TangentForLoginWithError('Cannot create user: [_1]', $msg);
+ TangentForLoginWithError($ARGS, 'Cannot create user: [_1]', $msg);
} else {
$m->abort();
}
@@ -668,13 +714,13 @@ sub AttemptExternalAuth {
$user = $orig_user;
unless ( RT->Config->Get('WebFallbackToInternalAuth') ) {
- TangentForLoginWithError('You are not an authorized user');
+ 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('You are not an authorized user');
+ TangentForLoginWithError($ARGS, 'You are not an authorized user');
}
} else {
@@ -705,7 +751,8 @@ sub AttemptPasswordAuthentication {
# It's important to nab the next page from the session before we blow
# the session away
- my $next = delete $HTML::Mason::Commands::session{'NextPage'}->{$ARGS->{'next'} || ''};
+ my $next = RemoveNextPage($ARGS->{'next'});
+ $next = $next->{'url'} if ref $next;
InstantiateNewSession();
$HTML::Mason::Commands::session{'CurrentUser'} = $user_obj;
@@ -1201,6 +1248,13 @@ our %is_whitelisted_component = (
'/m/tickets/search' => 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;
@@ -1223,6 +1277,10 @@ sub IsCompCSRFWhitelisted {
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.
@@ -1419,6 +1477,30 @@ sub MaybeShowInterstitialCSRFPage {
# 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/;
@@ -1645,9 +1727,8 @@ sub CreateTicket {
}
}
- foreach my $argument (qw(Encrypt Sign)) {
- $MIMEObj->head->replace( "X-RT-$argument" => $ARGS{$argument} ? 1 : 0 )
- if defined $ARGS{$argument};
+ for my $argument (qw(Encrypt Sign)) {
+ $MIMEObj->head->replace( "X-RT-$argument" => $ARGS{$argument} ? 1 : 0 );
}
my %create_args = (
diff --git a/rt/lib/RT/Interface/Web/Menu.pm b/rt/lib/RT/Interface/Web/Menu.pm
index 6b351e9..045df1f 100644
--- a/rt/lib/RT/Interface/Web/Menu.pm
+++ b/rt/lib/RT/Interface/Web/Menu.pm
@@ -150,10 +150,12 @@ treated as relative to it's parent's path, and made absolute.
sub path {
my $self = shift;
if (@_) {
- $self->{path} = shift;
- $self->{path} = URI->new_abs($self->{path}, $self->parent->path . "/")->as_string
- if defined $self->{path} and $self->parent and $self->parent->path;
- $self->{path} =~ s!///!/! if $self->{path};
+ if (defined($self->{path} = shift)) {
+ my $base = ($self->parent and $self->parent->path) ? $self->parent->path : "";
+ $base .= "/" unless $base =~ m{/$};
+ my $uri = URI->new_abs($self->{path}, $base);
+ $self->{path} = $uri->as_string;
+ }
}
return $self->{path};
}
@@ -230,6 +232,7 @@ sub child {
if ( defined $path and length $path ) {
my $base_path = $HTML::Mason::Commands::r->path_info;
my $query = $HTML::Mason::Commands::m->cgi_object->query_string;
+ $base_path =~ s!/+!/!g;
$base_path .= "?$query" if defined $query and length $query;
$base_path =~ s/index\.html$//;
diff --git a/rt/lib/RT/Pod/HTML.pm b/rt/lib/RT/Pod/HTML.pm
new file mode 100644
index 0000000..8ddce42
--- /dev/null
+++ b/rt/lib/RT/Pod/HTML.pm
@@ -0,0 +1,66 @@
+use strict;
+use warnings;
+
+package RT::Pod::HTML;
+use base 'Pod::Simple::XHTML';
+
+sub new {
+ my $self = shift->SUPER::new(@_);
+ $self->index(1);
+ $self->anchor_items(1);
+ return $self;
+}
+
+sub perldoc_url_prefix { "http://metacpan.org/module/" }
+
+sub html_header { '' }
+sub html_footer {
+ my $self = shift;
+ my $toc = "../" x ($self->batch_mode_current_level - 1);
+ return '<a href="./' . $toc . '">&larr; Back to index</a>';
+}
+
+sub start_Verbatim { $_[0]{'scratch'} = "<pre>" }
+sub end_Verbatim { $_[0]{'scratch'} .= "</pre>"; $_[0]->emit; }
+
+sub _end_head {
+ my $self = shift;
+ $self->{scratch} = '<a href="#___top">' . $self->{scratch} . '</a>';
+ return $self->SUPER::_end_head(@_);
+}
+
+sub resolve_pod_page_link {
+ my $self = shift;
+ my ($name, $section) = @_;
+
+ # Only try to resolve local links if we're in batch mode and are linking
+ # outside the current document.
+ return $self->SUPER::resolve_pod_page_link(@_)
+ unless $self->batch_mode and $name;
+
+ $section = defined $section
+ ? '#' . $self->idify($section, 1)
+ : '';
+
+ my $local;
+ if ($name =~ /^RT::/) {
+ $local = join "/",
+ map { $self->encode_entities($_) }
+ split /::/, $name;
+ }
+ elsif ($name =~ /^rt-/) {
+ $local = $self->encode_entities($name);
+ }
+
+ if ($local) {
+ # Resolve links correctly by going up
+ my $depth = $self->batch_mode_current_level - 1;
+ return join "/",
+ ($depth ? ".." x $depth : ()),
+ "$local.html$section";
+ } else {
+ return $self->SUPER::resolve_pod_page_link(@_)
+ }
+}
+
+1;
diff --git a/rt/lib/RT/Pod/HTMLBatch.pm b/rt/lib/RT/Pod/HTMLBatch.pm
new file mode 100644
index 0000000..8d1b67f
--- /dev/null
+++ b/rt/lib/RT/Pod/HTMLBatch.pm
@@ -0,0 +1,131 @@
+use strict;
+use warnings;
+
+package RT::Pod::HTMLBatch;
+use base 'Pod::Simple::HTMLBatch';
+
+use List::MoreUtils qw/all/;
+
+use RT::Pod::Search;
+use RT::Pod::HTML;
+
+sub new {
+ my $self = shift->SUPER::new(@_);
+ $self->verbose(0);
+
+ # Per-page output options
+ $self->css_flurry(0); # No CSS
+ $self->javascript_flurry(0); # No JS
+ $self->no_contents_links(1); # No header/footer "Back to contents" links
+
+ # TOC options
+ $self->index(1); # Write a per-page TOC
+ $self->contents_file("index.html"); # Write a global TOC
+
+ $self->html_render_class('RT::Pod::HTML');
+ $self->search_class('RT::Pod::Search');
+
+ return $self;
+}
+
+sub classify {
+ my $self = shift;
+ my %info = (@_);
+
+ my $is_install_doc = sub {
+ my %page = @_;
+ local $_ = $page{name};
+ return 1 if /^(README|UPGRADING)/;
+ return 1 if $_ eq "RT_Config";
+ return 1 if $_ eq "web_deployment";
+ return 1 if $page{infile} =~ m{^configure(\.ac)?$};
+ return 0;
+ };
+
+ my $section = $info{infile} =~ m{/plugins/([^/]+)} ? "05 Extension: $1" :
+ $info{infile} =~ m{/local/} ? '04 Local Documenation' :
+ $is_install_doc->(%info) ? '00 Install and Upgrade '.
+ 'Documentation' :
+ $info{infile} =~ m{/(docs|etc)/} ? '01 User Documentation' :
+ $info{infile} =~ m{/bin/} ? '02 Utilities (bin)' :
+ $info{infile} =~ m{/sbin/} ? '03 Utilities (sbin)' :
+ $info{name} =~ /^RT::Action/ ? '08 Actions' :
+ $info{name} =~ /^RT::Condition/ ? '09 Conditions' :
+ $info{name} =~ /^RT(::|$)/ ? '07 Developer Documentation' :
+ $info{infile} =~ m{/devel/tools/} ? '20 Utilities (devel/tools)' :
+ '06 Miscellaneous' ;
+
+ if ($info{infile} =~ m{/(docs|etc)/}) {
+ $info{name} =~ s/_/ /g;
+ $info{name} = join "/", map { ucfirst } split /::/, $info{name};
+ }
+
+ return ($info{name}, $section);
+}
+
+sub write_contents_file {
+ my ($self, $to) = @_;
+ return unless $self->contents_file;
+
+ my $file = join "/", $to, $self->contents_file;
+ open my $index, ">", $file
+ or warn "Unable to open index file '$file': $!\n", return;
+
+ my $pages = $self->_contents;
+ return unless @$pages;
+
+ # Classify
+ my %toc;
+ for my $page (@$pages) {
+ my ($name, $infile, $outfile, $pieces) = @$page;
+
+ my ($title, $section) = $self->classify(
+ name => $name,
+ infile => $infile,
+ );
+
+ (my $path = $outfile) =~ s{^\Q$to\E/?}{};
+
+ push @{ $toc{$section} }, {
+ name => $title,
+ path => $path,
+ };
+ }
+
+ # Write out index
+ print $index "<dl class='superindex'>\n";
+
+ for my $key (sort keys %toc) {
+ next unless @{ $toc{$key} };
+
+ (my $section = $key) =~ s/^\d+ //;
+ print $index "<dt>", esc($section), "</dt>\n";
+ print $index "<dd>\n";
+
+ my @sorted = sort {
+ my @names = map { $_->{name} } $a, $b;
+
+ # Sort just the upgrading docs descending within everything else
+ @names = reverse @names
+ if all { /^UPGRADING-/ } @names;
+
+ $names[0] cmp $names[1]
+ } @{ $toc{$key} };
+
+ for my $page (@sorted) {
+ print $index " <a href='", esc($page->{path}), "'>",
+ esc($page->{name}),
+ "</a><br>\n";
+ }
+ print $index "</dd>\n";
+ }
+ print $index '</dl>';
+
+ close $index;
+}
+
+sub esc {
+ Pod::Simple::HTMLBatch::esc(@_);
+}
+
+1;
diff --git a/rt/lib/RT/Pod/Search.pm b/rt/lib/RT/Pod/Search.pm
new file mode 100644
index 0000000..d6ddd2d
--- /dev/null
+++ b/rt/lib/RT/Pod/Search.pm
@@ -0,0 +1,15 @@
+use strict;
+use warnings;
+
+package RT::Pod::Search;
+use base 'Pod::Simple::Search';
+
+sub new {
+ my $self = shift->SUPER::new(@_);
+ $self->laborious(1) # Find scripts too
+ ->limit_re(qr/(?<!\.in)$/) # Filter out .in files
+ ->inc(0); # Don't look in @INC
+ return $self;
+}
+
+1;
diff --git a/rt/lib/RT/Queue.pm b/rt/lib/RT/Queue.pm
index 406df92..a942bb6 100755
--- a/rt/lib/RT/Queue.pm
+++ b/rt/lib/RT/Queue.pm
@@ -394,6 +394,7 @@ sub Create {
FinalPriority => 0,
DefaultDueIn => 0,
Sign => undef,
+ SignAuto => undef,
Encrypt => undef,
_RecordTransaction => 1,
@_
@@ -436,14 +437,11 @@ sub Create {
}
$RT::Handle->Commit;
- if ( defined $args{'Sign'} ) {
- my ($status, $msg) = $self->SetSign( $args{'Sign'} );
- $RT::Logger->error("Couldn't set attribute 'Sign': $msg")
- unless $status;
- }
- if ( defined $args{'Encrypt'} ) {
- my ($status, $msg) = $self->SetEncrypt( $args{'Encrypt'} );
- $RT::Logger->error("Couldn't set attribute 'Encrypt': $msg")
+ for my $attr (qw/Sign SignAuto Encrypt/) {
+ next unless defined $args{$attr};
+ my $set = "Set" . $attr;
+ my ($status, $msg) = $self->$set( $args{$attr} );
+ $RT::Logger->error("Couldn't set attribute '$attr': $msg")
unless $status;
}
@@ -595,6 +593,32 @@ sub SetSign {
return ($status, $self->loc('Signing disabled'));
}
+sub SignAuto {
+ my $self = shift;
+ my $value = shift;
+
+ return undef unless $self->CurrentUserHasRight('SeeQueue');
+ my $attr = $self->FirstAttribute('SignAuto') or return 0;
+ return $attr->Content;
+}
+
+sub SetSignAuto {
+ my $self = shift;
+ my $value = shift;
+
+ return ( 0, $self->loc('Permission Denied') )
+ unless $self->CurrentUserHasRight('AdminQueue');
+
+ my ($status, $msg) = $self->SetAttribute(
+ Name => 'SignAuto',
+ Description => 'Sign auto-generated outgoing messages',
+ Content => $value,
+ );
+ return ($status, $msg) unless $status;
+ return ($status, $self->loc('Signing enabled')) if $value;
+ return ($status, $self->loc('Signing disabled'));
+}
+
sub Encrypt {
my $self = shift;
my $value = shift;
diff --git a/rt/lib/RT/Record.pm b/rt/lib/RT/Record.pm
index fd238de..313888c 100755
--- a/rt/lib/RT/Record.pm
+++ b/rt/lib/RT/Record.pm
@@ -1483,8 +1483,35 @@ sub _DeleteLink {
}
+=head1 LockForUpdate
+In a database transaction, gains an exclusive lock on the row, to
+prevent race conditions. On SQLite, this is a "RESERVED" lock on the
+entire database.
+=cut
+
+sub LockForUpdate {
+ my $self = shift;
+
+ my $pk = $self->_PrimaryKey;
+ my $id = @_ ? $_[0] : $self->$pk;
+ $self->_expire if $self->isa("DBIx::SearchBuilder::Record::Cachable");
+ if (RT->Config->Get('DatabaseType') eq "SQLite") {
+ # SQLite does DB-level locking, upgrading the transaction to
+ # "RESERVED" on the first UPDATE/INSERT/DELETE. Do a no-op
+ # UPDATE to force the upgade.
+ return RT->DatabaseHandle->dbh->do(
+ "UPDATE " .$self->Table.
+ " SET $pk = $pk WHERE 1 = 0");
+ } else {
+ return $self->_LoadFromSQL(
+ "SELECT * FROM ".$self->Table
+ ." WHERE $pk = ? FOR UPDATE",
+ $id,
+ );
+ }
+}
=head2 _NewTransaction PARAMHASH
@@ -1512,6 +1539,11 @@ sub _NewTransaction {
@_
);
+ my $in_txn = RT->DatabaseHandle->TransactionDepth;
+ RT->DatabaseHandle->BeginTransaction unless $in_txn;
+
+ $self->LockForUpdate;
+
my $old_ref = $args{'OldReference'};
my $new_ref = $args{'NewReference'};
my $ref_type = $args{'ReferenceType'};
@@ -1559,6 +1591,9 @@ sub _NewTransaction {
if ( RT->Config->Get('UseTransactionBatch') and $transaction ) {
push @{$self->{_TransactionBatch}}, $trans if $args{'CommitScrips'};
}
+
+ RT->DatabaseHandle->Commit unless $in_txn;
+
return ( $transaction, $msg, $trans );
}
diff --git a/rt/lib/RT/Template.pm b/rt/lib/RT/Template.pm
index 117cc3f..e509454 100755
--- a/rt/lib/RT/Template.pm
+++ b/rt/lib/RT/Template.pm
@@ -390,6 +390,7 @@ sub _Parse {
# Unfold all headers
$self->{'MIMEObj'}->head->unfold;
+ $self->{'MIMEObj'}->head->modify(1);
return ( 1, $self->loc("Template parsed") );
diff --git a/rt/lib/RT/Ticket.pm b/rt/lib/RT/Ticket.pm
index 577c444..5f76e05 100755
--- a/rt/lib/RT/Ticket.pm
+++ b/rt/lib/RT/Ticket.pm
@@ -2199,14 +2199,16 @@ sub Comment {
}
$args{'NoteType'} = 'Comment';
+ $RT::Handle->BeginTransaction();
if ($args{'DryRun'}) {
- $RT::Handle->BeginTransaction();
$args{'CommitScrips'} = 0;
}
my @results = $self->_RecordNote(%args);
if ($args{'DryRun'}) {
$RT::Handle->Rollback();
+ } else {
+ $RT::Handle->Commit();
}
return(@results);
@@ -2245,10 +2247,10 @@ sub Correspond {
or ( $self->CurrentUserHasRight('ModifyTicket') ) ) {
return ( 0, $self->loc("Permission Denied"), undef );
}
+ $args{'NoteType'} = 'Correspond';
- $args{'NoteType'} = 'Correspond';
+ $RT::Handle->BeginTransaction();
if ($args{'DryRun'}) {
- $RT::Handle->BeginTransaction();
$args{'CommitScrips'} = 0;
}
@@ -2265,6 +2267,8 @@ sub Correspond {
if ($args{'DryRun'}) {
$RT::Handle->Rollback();
+ } else {
+ $RT::Handle->Commit();
}
return (@results);
diff --git a/rt/lib/RT/User.pm b/rt/lib/RT/User.pm
index e7f7c2a..f26ace4 100755
--- a/rt/lib/RT/User.pm
+++ b/rt/lib/RT/User.pm
@@ -102,6 +102,7 @@ sub _OverlayAccessible {
AuthSystem => { public => 1, admin => 1 },
Gecos => { public => 1, admin => 1 },
PGPKey => { public => 1, admin => 1 },
+ PrivateKey => { admin => 1 },
}
}