diff options
author | Ivan Kohler <ivan@freeside.biz> | 2012-12-12 10:26:29 -0800 |
---|---|---|
committer | Ivan Kohler <ivan@freeside.biz> | 2012-12-12 10:26:29 -0800 |
commit | 84f2df8931fa02e63fb21f8f0bb87dd9577b8919 (patch) | |
tree | 8cbe43418fe23f3fc28cc8f3e19a61666a1696ed /rt/lib | |
parent | d7678b9b97068dcd352f0ea101c6c8d02ae330d6 (diff) |
rt 4.0.8
Diffstat (limited to 'rt/lib')
-rwxr-xr-x | rt/lib/RT/Action/SendEmail.pm | 54 | ||||
-rw-r--r-- | rt/lib/RT/Approval/Rule/Passed.pm | 11 | ||||
-rw-r--r-- | rt/lib/RT/Article.pm | 2 | ||||
-rwxr-xr-x | rt/lib/RT/Attachment.pm | 49 | ||||
-rw-r--r-- | rt/lib/RT/Crypt/GnuPG.pm | 27 | ||||
-rw-r--r-- | rt/lib/RT/Generated.pm | 2 | ||||
-rw-r--r-- | rt/lib/RT/Handle.pm | 30 | ||||
-rwxr-xr-x | rt/lib/RT/Interface/Email.pm | 52 | ||||
-rwxr-xr-x | rt/lib/RT/Interface/Email/Auth/GnuPG.pm | 3 | ||||
-rw-r--r-- | rt/lib/RT/Interface/Web.pm | 117 | ||||
-rw-r--r-- | rt/lib/RT/Interface/Web/Menu.pm | 11 | ||||
-rw-r--r-- | rt/lib/RT/Pod/HTML.pm | 66 | ||||
-rw-r--r-- | rt/lib/RT/Pod/HTMLBatch.pm | 131 | ||||
-rw-r--r-- | rt/lib/RT/Pod/Search.pm | 15 | ||||
-rwxr-xr-x | rt/lib/RT/Queue.pm | 40 | ||||
-rwxr-xr-x | rt/lib/RT/Record.pm | 35 | ||||
-rwxr-xr-x | rt/lib/RT/Template.pm | 1 | ||||
-rwxr-xr-x | rt/lib/RT/Ticket.pm | 10 | ||||
-rwxr-xr-x | rt/lib/RT/User.pm | 1 |
19 files changed, 534 insertions, 123 deletions
diff --git a/rt/lib/RT/Action/SendEmail.pm b/rt/lib/RT/Action/SendEmail.pm index 4ae1a8b66..2a7a2e3c0 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 f364bc926..000a8dc62 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 24b952ad4..678aa1177 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 fb17da3b5..f1d9a6342 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 c5fb12bef..233047820 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 9fd946f5b..907ea77f6 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 99d10e367..03c262bba 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 4c3ee9986..dda6f704a 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 e508908fb..87a523dad 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 1aae7581e..745a6f1e3 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 6b351e94b..045df1fa0 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 000000000..8ddce42d1 --- /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 . '">← 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 000000000..8d1b67f34 --- /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 000000000..d6ddd2daf --- /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 406df9214..a942bb6d7 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 fd238de16..313888cbc 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 117cc3f1c..e509454b1 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 577c44429..5f76e055f 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 e7f7c2ad6..f26ace445 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 }, } } |