diff options
author | Alex Vandiver <alexmv@bestpractical.com> | 2012-04-26 04:06:49 +0400 |
---|---|---|
committer | Alex Vandiver <alexmv@bestpractical.com> | 2012-04-26 04:06:49 +0400 |
commit | fa9c4b4b218ea231c048312a3ca0be76b3231a1e (patch) | |
tree | aa9a898098f7635264df331e8d2eb66159584d0a | |
parent | 080eb2a7c4b4c790a315da18411d5f2d2d3818ba (diff) | |
parent | 21da57aba3248b21240954274bcf5d9a47c92b49 (diff) |
Merge branch 'security/4.0-trunk' into 4.0-trunkrt-4.0.6rc1
Conflicts:
lib/RT/Interface/Web.pm
112 files changed, 1678 insertions, 465 deletions
diff --git a/.gitignore b/.gitignore index 4fbbaa65f9..f62dd86458 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ /t/data/gnupg/keyrings/random_seed /t/data/configs/apache2.2+fastcgi.conf /t/data/configs/apache2.2+mod_perl.conf +/t/security/embargo/ /t/tmp/ /sbin/rt-attributes-viewer /sbin/rt-clean-sessions diff --git a/bin/rt-mailgate.in b/bin/rt-mailgate.in index 9ad129ae19..6e5f4a1765 100755 --- a/bin/rt-mailgate.in +++ b/bin/rt-mailgate.in @@ -172,7 +172,6 @@ sub setup_session { my $self = shift; my $opts = shift; my %post_params; - $post_params{SessionType} = 'REST'; # Surpress login box foreach (qw(queue action)) { $post_params{$_} = $opts->{$_} if defined $opts->{$_}; } diff --git a/docs/security.pod b/docs/security.pod index b8650e05d4..620f8687c6 100644 --- a/docs/security.pod +++ b/docs/security.pod @@ -9,6 +9,21 @@ key). More information is available at L<http://bestpractical.com/security/>. + +=head2 RT's security process + +After a security vulnerability is reported to Best Practical and +verified, we attempt to resolve it in as timely a fashion as possible. +Best Practical support customers will be notified before we disclose the +information to the public. All security announcements will be sent to +C<rt-announce@bestpractical.com>, which includes +C<rt-users@bestpractical.com> and C<rt-devel@bestpractical.com>. + +As the tests for security vulnerabilities are often nearly identical to +working exploits, sensitive tests will be embargoed for a period of six +months before being added to the public RT repository. + + =head2 Security tips for running RT =over diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in index 50b46c324b..c560c5eacc 100755 --- a/etc/RT_Config.pm.in +++ b/etc/RT_Config.pm.in @@ -1758,8 +1758,33 @@ This disables RT's clickjacking protection. Set($Framebusting, 1); +=item C<$RestrictReferrer> + +If set to a false value, the HTTP C<Referer> (sic) header will not be +checked to ensure that requests come from RT's own domain. As RT allows +for GET requests to alter state, disabling this opens RT up to +cross-site request forgery (CSRF) attacks. + +=cut + +Set($RestrictReferrer, 1); + +=item C<$RestrictLoginReferrer> + +If set to a false value, RT will allow the user to log in from any link +or request, merely by passing in C<user> and C<pass> parameters; setting +it to a true value forces all logins to come from the login box, so the +user us aware that they are being logged in. The default is off, for +backwards compatability. + +=cut + +Set($RestrictLoginReferrer, 0); + =back + + =head1 Authorization and user configuration =over 4 diff --git a/etc/upgrade/4.0.6/content b/etc/upgrade/4.0.6/content new file mode 100644 index 0000000000..dc1a009518 --- /dev/null +++ b/etc/upgrade/4.0.6/content @@ -0,0 +1,17 @@ +@Initial = ( + sub { + my $txns = RT::Transactions->new( $RT::SystemUser ); + $txns->Limit( + FIELD => "ObjectType", + VALUE => "RT::User", + ); + $txns->Limit( + FIELD => "Field", + VALUE => "Password", + ); + while (my $txn = $txns->Next) { + $txn->__Set( Field => $_, Value => '********' ) + for qw/OldValue NewValue/; + } + }, +); diff --git a/etc/upgrade/vulnerable-passwords.in b/etc/upgrade/vulnerable-passwords.in index 728786fb69..a3d719c318 100755 --- a/etc/upgrade/vulnerable-passwords.in +++ b/etc/upgrade/vulnerable-passwords.in @@ -89,6 +89,9 @@ push @{$users->{'restrictions'}{ "main.Password" }}, "AND", { value => '40', }; +# we want to update passwords on disabled users +$users->{'find_disabled_rows'} = 1; + my $count = $users->Count; if ($count == 0) { print "No users with unsalted or weak cryptography found.\n"; @@ -679,11 +679,21 @@ sub InitPlugins { sub InstallMode { my $self = shift; if (@_) { - $_INSTALL_MODE = shift; - if($_INSTALL_MODE) { - require RT::CurrentUser; - $SystemUser = RT::CurrentUser->new(); - } + my ($integrity, $state, $msg) = RT::Handle->CheckIntegrity; + if ($_[0] and $integrity) { + # Trying to turn install mode on but we have a good DB! + require Carp; + $RT::Logger->error( + Carp::longmess("Something tried to turn on InstallMode but we have DB integrity!") + ); + } + else { + $_INSTALL_MODE = shift; + if($_INSTALL_MODE) { + require RT::CurrentUser; + $SystemUser = RT::CurrentUser->new(); + } + } } return $_INSTALL_MODE; } diff --git a/lib/RT/ACL.pm b/lib/RT/ACL.pm index d7c9ef2a81..49a7f1d64a 100644 --- a/lib/RT/ACL.pm +++ b/lib/RT/ACL.pm @@ -182,6 +182,9 @@ sub LimitToPrincipal { ALIAS2 => $cgm, FIELD2 => 'GroupId' ); + $self->Limit( ALIAS => $cgm, + FIELD => 'Disabled', + VALUE => 0 ); $self->Limit( ALIAS => $cgm, FIELD => 'MemberId', OPERATOR => '=', diff --git a/lib/RT/Action/CreateTickets.pm b/lib/RT/Action/CreateTickets.pm index 19aa134874..34a6217996 100644 --- a/lib/RT/Action/CreateTickets.pm +++ b/lib/RT/Action/CreateTickets.pm @@ -325,9 +325,19 @@ sub Prepare { } + 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 => 1 + _ActiveContent => $active, ); return 1; @@ -1170,6 +1180,7 @@ sub UpdateCustomFields { my $cf = $1; my $CustomFieldObj = RT::CustomField->new($self->CurrentUser); + $CustomFieldObj->SetContextObject( $ticket ); $CustomFieldObj->LoadById($cf); my @values; diff --git a/lib/RT/Action/SendEmail.pm b/lib/RT/Action/SendEmail.pm index e2aa00bb6e..94686b894e 100644 --- a/lib/RT/Action/SendEmail.pm +++ b/lib/RT/Action/SendEmail.pm @@ -348,7 +348,7 @@ sub AddAttachments { $MIMEObj->head->delete('RT-Attach-Message'); - my $attachments = RT::Attachments->new(RT->SystemUser); + my $attachments = RT::Attachments->new( $self->TransactionObj->CreatorObj ); $attachments->Limit( FIELD => 'TransactionId', VALUE => $self->TransactionObj->Id @@ -408,6 +408,10 @@ sub AddAttachment { 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 my $disp = ($attach->GetHeader('Content-Disposition') || '') =~ /^\s*(inline|attachment)/i ? $1 : undef; @@ -471,8 +475,7 @@ sub AddTicket { my $self = shift; my $tid = shift; - # XXX: we need a current user here, but who is current user? - my $attachs = RT::Attachments->new(RT->SystemUser); + my $attachs = RT::Attachments->new( $self->TransactionObj->CreatorObj ); my $txn_alias = $attachs->TransactionAlias; $attachs->Limit( ALIAS => $txn_alias, FIELD => 'Type', VALUE => 'Create' ); $attachs->Limit( diff --git a/lib/RT/Article.pm b/lib/RT/Article.pm index 7310241ee3..24b952ad4e 100644 --- a/lib/RT/Article.pm +++ b/lib/RT/Article.pm @@ -543,6 +543,17 @@ sub CurrentUserHasRight { } +=head2 CurrentUserCanSee + +Returns true if the current user can see the article, using ShowArticle + +=cut + +sub CurrentUserCanSee { + my $self = shift; + return $self->CurrentUserHasRight('ShowArticle'); +} + # }}} # {{{ _Set diff --git a/lib/RT/Attachments.pm b/lib/RT/Attachments.pm index c640b206e9..2bdbc244c3 100644 --- a/lib/RT/Attachments.pm +++ b/lib/RT/Attachments.pm @@ -227,15 +227,12 @@ sub Next { my $Attachment = $self->SUPER::Next; return $Attachment unless $Attachment; - my $txn = $Attachment->TransactionObj; - if ( $txn->__Value('Type') eq 'Comment' ) { - return $Attachment if $txn->CurrentUserHasRight('ShowTicketComments'); - } elsif ( $txn->CurrentUserHasRight('ShowTicket') ) { + if ( $Attachment->TransactionObj->CurrentUserCanSee ) { return $Attachment; + } else { + # If the user doesn't have the right to show this ticket + return $self->Next; } - - # If the user doesn't have the right to show this ticket - return $self->Next; } diff --git a/lib/RT/Class.pm b/lib/RT/Class.pm index bb694ce9cb..3906b9fed5 100644 --- a/lib/RT/Class.pm +++ b/lib/RT/Class.pm @@ -275,6 +275,7 @@ sub ArticleCustomFields { my $cfs = RT::CustomFields->new( $self->CurrentUser ); if ( $self->CurrentUserHasRight('SeeClass') ) { + $cfs->SetContextObject( $self ); $cfs->LimitToGlobalOrObjectId( $self->Id ); $cfs->LimitToLookupType( RT::Article->CustomFieldLookupType ); $cfs->ApplySortOrder; diff --git a/lib/RT/CustomField.pm b/lib/RT/CustomField.pm index 91afa3e3dc..2002d4e5ba 100644 --- a/lib/RT/CustomField.pm +++ b/lib/RT/CustomField.pm @@ -465,10 +465,12 @@ sub LoadByName { } # if we're looking for a queue by name, make it a number - if ( defined $args{'Queue'} && $args{'Queue'} =~ /\D/ ) { + if ( defined $args{'Queue'} && ($args{'Queue'} =~ /\D/ || !$self->ContextObject) ) { my $QueueObj = RT::Queue->new( $self->CurrentUser ); $QueueObj->Load( $args{'Queue'} ); $args{'Queue'} = $QueueObj->Id; + $self->SetContextObject( $QueueObj ) + unless $self->ContextObject; } # XXX - really naive implementation. Slow. - not really. still just one query @@ -526,6 +528,8 @@ sub Values { # if the user has no rights, return an empty object if ( $self->id && $self->CurrentUserHasRight( 'SeeCustomField') ) { $cf_values->LimitToCustomField( $self->Id ); + } else { + $cf_values->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' ); } return ($cf_values); } @@ -881,7 +885,77 @@ sub ContextObject { my $self = shift; return $self->{'context_object'}; } - + +sub ValidContextType { + my $self = shift; + my $class = shift; + + my %valid; + $valid{$_}++ for split '-', $self->LookupType; + delete $valid{'RT::Transaction'}; + + return $valid{$class}; +} + +=head2 LoadContextObject + +Takes an Id for a Context Object and loads the right kind of RT::Object +for this particular Custom Field (based on the LookupType) and returns it. +This is a good way to ensure you don't try to use a Queue as a Context +Object on a User Custom Field. + +=cut + +sub LoadContextObject { + my $self = shift; + my $type = shift; + my $contextid = shift; + + unless ( $self->ValidContextType($type) ) { + RT->Logger->debug("Invalid ContextType $type for Custom Field ".$self->Id); + return; + } + + my $context_object = $type->new( $self->CurrentUser ); + my ($id, $msg) = $context_object->LoadById( $contextid ); + unless ( $id ) { + RT->Logger->debug("Invalid ContextObject id: $msg"); + return; + } + return $context_object; +} + +=head2 ValidateContextObject + +Ensure that a given ContextObject applies to this Custom Field. +For custom fields that are assigned to Queues or to Classes, this checks that the Custom +Field is actually applied to that objects. For Global Custom Fields, it returns true +as long as the Object is of the right type, because you may be using +your permissions on a given Queue of Class to see a Global CF. +For CFs that are only applied Globally, you don't need a ContextObject. + +=cut + +sub ValidateContextObject { + my $self = shift; + my $object = shift; + + return 1 if $self->IsApplied(0); + + # global only custom fields don't have objects + # that should be used as context objects. + return if $self->ApplyGlobally; + + # Otherwise, make sure we weren't passed a user object that we're + # supposed to treat as a queue. + return unless $self->ValidContextType(ref $object); + + # Check that it is applied correctly + my ($applied_to) = grep {ref($_) eq $self->RecordClassFromLookupType} ($object, $object->ACLEquivalenceObjects); + return unless $applied_to; + return $self->IsApplied($applied_to->id); +} + sub _Set { my $self = shift; @@ -1693,6 +1767,7 @@ sub SetBasedOn { unless defined $value and length $value; my $cf = RT::CustomField->new( $self->CurrentUser ); + $cf->SetContextObject( $self->ContextObject ); $cf->Load( ref $value ? $value->id : $value ); return (0, "Permission denied") @@ -1710,6 +1785,7 @@ sub BasedOnObj { my $self = shift; my $obj = RT::CustomField->new( $self->CurrentUser ); + $obj->SetContextObject( $self->ContextObject ); if ( $self->BasedOn ) { $obj->Load( $self->BasedOn ); } diff --git a/lib/RT/Dashboard/Mailer.pm b/lib/RT/Dashboard/Mailer.pm index c7e6ee309f..40b53b1110 100644 --- a/lib/RT/Dashboard/Mailer.pm +++ b/lib/RT/Dashboard/Mailer.pm @@ -447,6 +447,9 @@ sub BuildEmail { autohandler_name => '', # disable forced login and more data_dir => $data_dir, ); + $mason->set_escape( h => \&RT::Interface::Web::EscapeUTF8 ); + $mason->set_escape( u => \&RT::Interface::Web::EscapeURI ); + $mason->set_escape( j => \&RT::Interface::Web::EscapeJS ); } return $mason; } diff --git a/lib/RT/Date.pm b/lib/RT/Date.pm index 2ebbe85caf..ed094d0779 100644 --- a/lib/RT/Date.pm +++ b/lib/RT/Date.pm @@ -545,6 +545,10 @@ sub Get my $self = shift; my %args = (Format => 'ISO', @_); my $formatter = $args{'Format'}; + unless ( $self->ValidFormatter($formatter) ) { + RT->Logger->warning("Invalid date formatter '$formatter', falling back to ISO"); + $formatter = 'ISO'; + } $formatter = 'ISO' unless $self->can($formatter); return $self->$formatter( %args ); } @@ -583,6 +587,20 @@ sub Formatters return @FORMATTERS; } +=head3 ValidFormatter FORMAT + +Returns a true value if C<FORMAT> is a known formatter. Otherwise returns +false. + +=cut + +sub ValidFormatter { + my $self = shift; + my $format = shift; + return (grep { $_ eq $format } $self->Formatters and $self->can($format)) + ? 1 : 0; +} + =head3 DefaultFormat =cut @@ -661,15 +679,19 @@ sub LocalizedDateTime my %args = ( Date => 1, Time => 1, Timezone => '', - DateFormat => 'date_format_full', - TimeFormat => 'time_format_medium', + DateFormat => '', + TimeFormat => '', AbbrDay => 1, AbbrMonth => 1, @_, ); - my $date_format = $args{'DateFormat'}; - my $time_format = $args{'TimeFormat'}; + # Require valid names for the format methods + my $date_format = $args{DateFormat} =~ /^\w+$/ + ? $args{DateFormat} : 'date_format_full'; + + my $time_format = $args{TimeFormat} =~ /^\w+$/ + ? $args{TimeFormat} : 'time_format_medium'; my $formatter = $self->LocaleObj; $date_format = $formatter->$date_format; diff --git a/lib/RT/Graph/Tickets.pm b/lib/RT/Graph/Tickets.pm index 112934ea37..b839824f98 100644 --- a/lib/RT/Graph/Tickets.pm +++ b/lib/RT/Graph/Tickets.pm @@ -100,7 +100,7 @@ EOT sub gv_escape($) { my $value = shift; - $value =~ s{(?=")}{\\}g; + $value =~ s{(?=["\\])}{\\}g; return $value; } @@ -278,6 +278,14 @@ sub TicketLinks { ShowLinkDescriptions => 0, @_ ); + + my %valid_links = map { $_ => 1 } + qw(Members MemberOf RefersTo ReferredToBy DependsOn DependedOnBy); + + # Validate our link types + $args{ShowLinks} = [ grep { $valid_links{$_} } @{$args{ShowLinks}} ]; + $args{LeadingLink} = 'Members' unless $valid_links{ $args{LeadingLink} }; + unless ( $args{'Graph'} ) { $args{'Graph'} = GraphViz->new( name => 'ticket_links_'. $args{'Ticket'}->id, diff --git a/lib/RT/Group.pm b/lib/RT/Group.pm index 779c026485..b367b2f967 100644 --- a/lib/RT/Group.pm +++ b/lib/RT/Group.pm @@ -1171,8 +1171,18 @@ sub CurrentUserHasRight { } +=head2 CurrentUserCanSee +Always returns 1; unfortunately, for historical reasons, users have +always been able to examine groups they have indirect access to, even if +they do not have SeeGroup explicitly. +=cut + +sub CurrentUserCanSee { + my $self = shift; + return 1; +} =head2 PrincipalObj diff --git a/lib/RT/Groups.pm b/lib/RT/Groups.pm index 1ac0180972..1bb571b6a8 100644 --- a/lib/RT/Groups.pm +++ b/lib/RT/Groups.pm @@ -234,6 +234,8 @@ sub WithMember { ALIAS2 => $members, FIELD2 => 'GroupId'); $self->Limit(ALIAS => $members, FIELD => 'MemberId', OPERATOR => '=', VALUE => $args{'PrincipalId'}); + $self->Limit(ALIAS => $members, FIELD => 'Disabled', VALUE => 0) + if $args{'Recursively'}; return $members; } @@ -261,6 +263,12 @@ sub WithoutMember { VALUE => $args{'PrincipalId'}, ); $self->Limit( + LEFTJOIN => $members_alias, + ALIAS => $members_alias, + FIELD => 'Disabled', + VALUE => 0 + ) if $args{'Recursively'}; + $self->Limit( ALIAS => $members_alias, FIELD => 'MemberId', OPERATOR => 'IS', diff --git a/lib/RT/Handle.pm b/lib/RT/Handle.pm index 4955bb7502..99d10e3674 100644 --- a/lib/RT/Handle.pm +++ b/lib/RT/Handle.pm @@ -226,14 +226,12 @@ sub CheckIntegrity { my $self = shift; $self = new $self unless ref $self; - do { + unless ($RT::Handle and $RT::Handle->dbh) { local $@; unless ( eval { RT::ConnectToDatabase(); 1 } ) { return (0, 'no connection', "$@"); } - }; - - RT::InitLogging(); + } require RT::CurrentUser; my $test_user = RT::CurrentUser->new; diff --git a/lib/RT/Interface/Email.pm b/lib/RT/Interface/Email.pm index 909a9f423f..385ba7271f 100644 --- a/lib/RT/Interface/Email.pm +++ b/lib/RT/Interface/Email.pm @@ -57,6 +57,7 @@ use RT::EmailParser; use File::Temp; use UNIVERSAL::require; use Mail::Mailer (); +use Text::ParseWords qw/shellwords/; BEGIN { use base 'Exporter'; @@ -404,11 +405,11 @@ sub SendEmail { if ( $mail_command eq 'sendmailpipe' ) { my $path = RT->Config->Get('SendmailPath'); - my $args = RT->Config->Get('SendmailArguments'); + my @args = shellwords(RT->Config->Get('SendmailArguments')); # SetOutgoingMailFrom and bounces conflict, since they both want -f if ( $args{'Bounce'} ) { - $args .= ' '. RT->Config->Get('SendmailBounceArguments'); + push @args, shellwords(RT->Config->Get('SendmailBounceArguments')); } elsif ( RT->Config->Get('SetOutgoingMailFrom') ) { my $OutgoingMailAddress; @@ -425,7 +426,7 @@ sub SendEmail { $OutgoingMailAddress ||= RT->Config->Get('OverrideOutgoingMailFrom')->{'Default'}; - $args .= " -f $OutgoingMailAddress" + push @args, "-f", $OutgoingMailAddress if $OutgoingMailAddress; } @@ -437,32 +438,36 @@ sub SendEmail { my $from = $TransactionObj->CreatorObj->EmailAddress; $from =~ s/@/=/g; $from =~ s/\s//g; - $args .= " -f $prefix$from\@$domain"; + push @args, "-f", "$prefix$from\@$domain"; } eval { # don't ignore CHLD signal to get proper exit code local $SIG{'CHLD'} = 'DEFAULT'; - open( my $mail, '|-', "$path $args >/dev/null" ) - or die "couldn't execute program: $!"; - # 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: $!"; - unless ( close $mail ) { - die "close pipe failed: $!" if $!; # system error + 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` exitted with code ". ($?>>8); + 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`: " . $@ ); + $RT::Logger->crit( "$msgid: Could not send mail with command `$path @args`: " . $@ ); if ( $TicketObj ) { _RecordSendEmailFailure( $TicketObj ); } diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm index 8e89ce8dea..0514d629b4 100644 --- a/lib/RT/Interface/Web.pm +++ b/lib/RT/Interface/Web.pm @@ -158,6 +158,25 @@ 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 @@ -236,6 +255,7 @@ sub HandleRequest { DecodeARGS($ARGS); PreprocessTimeUpdates($ARGS); + InitializeMenu(); MaybeShowInstallModePage(); $HTML::Mason::Commands::m->comp( '/Elements/SetupSessionCookie', %$ARGS ); @@ -285,6 +305,8 @@ sub HandleRequest { } } + 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'} ); @@ -345,8 +367,6 @@ sub SetNextPage { $HTML::Mason::Commands::session{'NextPage'}->{$hash} = $next; $HTML::Mason::Commands::session{'i'}++; - - SendSessionCookie(); return $hash; } @@ -463,7 +483,6 @@ sub MaybeShowNoAuthPage { if $m->base_comp->path eq '/NoAuth/Login.html' and _UserLoggedIn(); # If it's a noauth file, don't ask for auth. - SendSessionCookie(); $m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %$ARGS ); $m->abort; } @@ -490,7 +509,7 @@ sub MaybeRejectPrivateComponentRequest { _elements | # mobile UI Widgets | autohandler | # requesting this directly is suspicious - l ) # loc component + l (_unsafe)? ) # loc component ( $ | / ) # trailing slash or end of path }xi) { $m->abort(403); @@ -523,10 +542,6 @@ sub ShowRequestedPage { # precache all system level rights for the current user $HTML::Mason::Commands::session{CurrentUser}->PrincipalObj->HasRights( Object => RT->System ); - InitializeMenu(); - - SendSessionCookie(); - # If the user isn't privileged, they can only see SelfService unless ( $HTML::Mason::Commands::session{'CurrentUser'}->Privileged ) { @@ -811,6 +826,10 @@ sub StaticFileHeaders { # make cache public $HTML::Mason::Commands::r->headers_out->{'Cache-Control'} = 'max-age=259200, public'; + # 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. $date->Set( Value => time + 30 * 24 * 60 * 60 ); $HTML::Mason::Commands::r->headers_out->{'Expires'} = $date->RFC2616; @@ -822,6 +841,22 @@ sub StaticFileHeaders { # $HTML::Mason::Commands::r->headers_out->{'Last-Modified'} = $date->RFC2616; } +=head2 ComponentPathIsSafe PATH + +Takes C<PATH> 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<.>) and does +not contain a slash-dot C</.>. + +=cut + +sub ComponentPathIsSafe { + my $self = shift; + my $path = shift; + return $path !~ m{(?:^|/)\.}; +} + =head2 PathIsSafe Takes a C<< Path => path >> and returns a boolean indicating that @@ -1126,6 +1161,192 @@ sub ComponentRoots { return @roots; } +my %is_whitelisted_path = ( + # 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, +); + +sub IsCompCSRFWhitelisted { + my $comp = shift; + my $ARGS = shift; + + return 1 if $is_whitelisted_path{$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}; + } + + # 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 valid results= from MaybeRedirectForResults, that's + # also fine. + delete $args{results} if $args{results} + and $HTML::Mason::Commands::session{"Actions"}->{$args{results}}; + + # 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 $config = _NormalizeHost(RT->Config->Get('WebBaseURL')); + + return (1,$referer,$config) if $referer->host_port eq $config->host_port; + + return (0,$referer,$config); +} + +=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 $uri= URI->new(shift); + $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 = <<EOT; +This login session belongs to a REST client, and cannot be used to +access non-REST interfaces of RT for security reasons. +EOT + my $details = <<EOT; +Please log out and back in to obtain a session for normal browsing. If +you understand the security implications, disabling RT's CSRF protection +will remove this restriction. +EOT + chomp $details; + HTML::Mason::Commands::Abort( $why, Details => $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, $config) = IsRefererCSRFWhitelisted($ENV{HTTP_REFERER}); + return 0 if $whitelisted; + + return (1, + "the Referrer header supplied by your browser ([_1]) is not allowed by RT's configured hostname ([_2])", # loc + $browser->host_port, $config->host_port); +} + +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}}; + + # 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 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 = 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'}++; + + $HTML::Mason::Commands::m->comp( + '/Elements/CSRF', + OriginalURL => $HTML::Mason::Commands::r->path_info, + Reason => HTML::Mason::Commands::loc( $msg, @loc ), + Token => $token, + ); + # Calls abort, never gets here +} + package HTML::Mason::Commands; use vars qw/$r $m %session/; @@ -1406,6 +1627,7 @@ sub CreateTicket { 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 ); @@ -2220,6 +2442,7 @@ sub ProcessObjectCustomFieldUpdates { 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"); @@ -2826,52 +3049,71 @@ sub ScrubHTML { =head2 _NewScrubber -Returns a new L<HTML::Scrubber> object. Override this if you insist on -letting more HTML through. +Returns a new L<HTML::Scrubber> object. + +If you need to be more lax about what HTML tags and attributes are allowed, +create C</opt/rt4/local/lib/RT/Interface/Web_Local.pm> 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, ftp and relative urls + # XXX: we also scrub format strings with this module then allow simple config options + href => qr{^(?:http:|ftp:|https:|/|__Web(?:Path|BaseURL|URL)__)}i, + face => 1, + size => 1, + target => 1, + style => qr{ + ^(?:\s* + (?:(?:background-)?color: \s* + (?:rgb\(\s* \d+, \s* \d+, \s* \d+ \s*\) | # rgb(d,d,d) + \#[a-f0-9]{3,6} | # #fff or #ffffff + [\w\-]+ # green, light-blue, etc. + ) | + text-align: \s* \w+ | + font-size: \s* [\w.\-]+ | + font-family: \s* [\w\s"',.\-]+ | + font-weight: \s* [\w\-]+ | + + # MS Office styles, which are probably fine. If we don't, then any + # associated styles in the same attribute get stripped. + mso-[\w\-]+?: \s* [\w\s"',.\-]+ + )\s* ;? \s*) + +$ # one or more of these allowed properties from here 'till sunset + }ix, + 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, { - '*' => 0, - id => 1, - class => 1, - # Match http, ftp and relative urls - # XXX: we also scrub format strings with this module then allow simple config options - href => qr{^(?:http:|ftp:|https:|/|__Web(?:Path|BaseURL|URL)__)}i, - face => 1, - size => 1, - target => 1, - style => qr{ - ^(?:\s* - (?:(?:background-)?color: \s* - (?:rgb\(\s* \d+, \s* \d+, \s* \d+ \s*\) | # rgb(d,d,d) - \#[a-f0-9]{3,6} | # #fff or #ffffff - [\w\-]+ # green, light-blue, etc. - ) | - text-align: \s* \w+ | - font-size: \s* [\w.\-]+ | - font-family: \s* [\w\s"',.\-]+ | - font-weight: \s* [\w\-]+ | - - # MS Office styles, which are probably fine. If we don't, then any - # associated styles in the same attribute get stripped. - mso-[\w\-]+?: \s* [\w\s"',.\-]+ - )\s* ;? \s*) - +$ # one or more of these allowed properties from here 'till sunset - }ix, - dir => qr/^(rtl|ltr)$/i, - lang => qr/^\w+(-\w+)?$/, - } + %SCRUBBER_ALLOWED_ATTRIBUTES, + '*' => 0, # require attributes be explicitly allowed + }, ); $scrubber->deny(qw[*]); - $scrubber->allow( - qw[A B U P BR I HR BR SMALL EM FONT SPAN STRONG SUB SUP STRIKE H1 H2 H3 H4 H5 H6 DIV UL OL LI DL DT DD PRE BLOCKQUOTE BDO] - ); + $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; diff --git a/lib/RT/Interface/Web/Handler.pm b/lib/RT/Interface/Web/Handler.pm index 69eee60f6c..f96f66e70d 100644 --- a/lib/RT/Interface/Web/Handler.pm +++ b/lib/RT/Interface/Web/Handler.pm @@ -74,7 +74,7 @@ sub DefaultHandlerArgs { ( static_source => (RT->Config->Get('DevelMode') ? '0' : '1'), use_object_files => (RT->Config->Get('DevelMode') ? '0' : '1'), autoflush => 0, - error_format => (RT->Config->Get('DevelMode') ? 'html': 'brief'), + error_format => (RT->Config->Get('DevelMode') ? 'html': 'rt_error'), request_class => 'RT::Interface::Web::Request', named_component_subs => $INC{'Devel/Cover.pm'} ? 1 : 0, ) }; @@ -116,6 +116,7 @@ sub NewHandler { $handler->interp->set_escape( h => \&RT::Interface::Web::EscapeUTF8 ); $handler->interp->set_escape( u => \&RT::Interface::Web::EscapeURI ); + $handler->interp->set_escape( j => \&RT::Interface::Web::EscapeJS ); return($handler); } @@ -202,6 +203,13 @@ sub CleanupRequest { } +sub HTML::Mason::Exception::as_rt_error { + my ($self) = @_; + $RT::Logger->error( $self->full_message ); + return "An internal RT error has occurred. Your administrator can find more details in RT's log files."; +} + + # PSGI App use RT::Interface::Web::Handler; diff --git a/lib/RT/ObjectCustomField.pm b/lib/RT/ObjectCustomField.pm index 0b815aef3c..61bc35532b 100644 --- a/lib/RT/ObjectCustomField.pm +++ b/lib/RT/ObjectCustomField.pm @@ -137,7 +137,19 @@ Returns the CustomField Object which has the id returned by CustomField sub CustomFieldObj { my $self = shift; my $id = shift || $self->CustomField; + + # To find out the proper context object to load the CF with, we need + # data from the CF -- namely, the record class. Go find that as the + # system user first. + my $system_CF = RT::CustomField->new( RT->SystemUser ); + $system_CF->Load( $id ); + my $class = $system_CF->RecordClassFromLookupType; + + my $obj = $class->new( $self->CurrentUser ); + $obj->Load( $self->ObjectId ); + my $CF = RT::CustomField->new( $self->CurrentUser ); + $CF->SetContextObject( $obj ); $CF->Load( $id ); return $CF; } diff --git a/lib/RT/ObjectCustomFieldValue.pm b/lib/RT/ObjectCustomFieldValue.pm index 0fd9d735c3..98714a0480 100644 --- a/lib/RT/ObjectCustomFieldValue.pm +++ b/lib/RT/ObjectCustomFieldValue.pm @@ -251,6 +251,8 @@ my $re_ip_serialized = qr/$re_ip_sunit(?:\.$re_ip_sunit){3}/; sub Content { my $self = shift; + return undef unless $self->CustomFieldObj->CurrentUserHasRight('SeeCustomField'); + my $content = $self->_Value('Content'); if ( $self->CustomFieldObj->Type eq 'IPAddress' || $self->CustomFieldObj->Type eq 'IPAddressRange' ) @@ -364,11 +366,11 @@ sub _FillInTemplateURL { # special case, whole value should be an URL if ( $url =~ /^__CustomField__/ ) { my $value = $self->Content; - # protect from javascript: URLs - if ( $value =~ /^\s*javascript:/i ) { + # protect from potentially malicious URLs + if ( $value =~ /^\s*(?:javascript|data):/i ) { my $object = $self->Object; $RT::Logger->error( - "Dangerouse value with JavaScript in custom field '". $self->CustomFieldObj->Name ."'" + "Potentially dangerous URL type in custom field '". $self->CustomFieldObj->Name ."'" ." on ". ref($object) ." #". $object->id ); return undef; diff --git a/lib/RT/Queue.pm b/lib/RT/Queue.pm index 3cb87c4be1..406df9214f 100644 --- a/lib/RT/Queue.pm +++ b/lib/RT/Queue.pm @@ -692,6 +692,7 @@ sub TicketTransactionCustomFields { my $cfs = RT::CustomFields->new( $self->CurrentUser ); if ( $self->CurrentUserHasRight('SeeQueue') ) { + $cfs->SetContextObject( $self ); $cfs->LimitToGlobalOrObjectId( $self->Id ); $cfs->LimitToLookupType( 'RT::Queue-RT::Ticket-RT::Transaction' ); $cfs->ApplySortOrder; @@ -1249,6 +1250,17 @@ sub CurrentUserHasRight { } +=head2 CurrentUserCanSee + +Returns true if the current user can see the queue, using SeeQueue + +=cut + +sub CurrentUserCanSee { + my $self = shift; + + return $self->CurrentUserHasRight('SeeQueue'); +} =head2 HasRight diff --git a/lib/RT/Scrip.pm b/lib/RT/Scrip.pm index 3e8f352dc9..0e0c7a03c3 100644 --- a/lib/RT/Scrip.pm +++ b/lib/RT/Scrip.pm @@ -510,13 +510,35 @@ sub _Set { } - if (length($args{Value})) { + if (exists $args{Value}) { if ($args{Field} eq 'CustomIsApplicableCode' || $args{Field} eq 'CustomPrepareCode' || $args{Field} eq 'CustomCommitCode') { unless ( $self->CurrentUser->HasRight( Object => $RT::System, Right => 'ExecuteCode' ) ) { return ( 0, $self->loc('Permission Denied') ); } } + elsif ($args{Field} eq 'Queue') { + if ($args{Value}) { + # moving to another queue + my $queue = RT::Queue->new( $self->CurrentUser ); + $queue->Load($args{Value}); + unless ($queue->Id and $queue->CurrentUserHasRight('ModifyScrips')) { + return ( 0, $self->loc('Permission Denied') ); + } + } else { + # moving to global + unless ($self->CurrentUser->HasRight( Object => RT->System, Right => 'ModifyScrips' )) { + return ( 0, $self->loc('Permission Denied') ); + } + } + } + elsif ($args{Field} eq 'Template') { + my $template = RT::Template->new( $self->CurrentUser ); + $template->Load($args{Value}); + unless ($template->Id and $template->CurrentUserCanRead) { + return ( 0, $self->loc('Permission Denied') ); + } + } } return $self->__Set(@_); diff --git a/lib/RT/SearchBuilder.pm b/lib/RT/SearchBuilder.pm index aa2e25ae9e..5ee7ecb140 100644 --- a/lib/RT/SearchBuilder.pm +++ b/lib/RT/SearchBuilder.pm @@ -131,6 +131,19 @@ sub OrderByCols { return $self->SUPER::OrderByCols( @sort ); } +# If we're setting RowsPerPage or FirstRow, ensure we get a natural number or undef. +sub RowsPerPage { + my $self = shift; + return if @_ and defined $_[0] and $_[0] =~ /\D/; + return $self->SUPER::RowsPerPage(@_); +} + +sub FirstRow { + my $self = shift; + return if @_ and defined $_[0] and $_[0] =~ /\D/; + return $self->SUPER::FirstRow(@_); +} + =head2 LimitToEnabled Only find items that haven't been disabled diff --git a/lib/RT/Shredder.pm b/lib/RT/Shredder.pm index 10d353677a..40c73b36d4 100644 --- a/lib/RT/Shredder.pm +++ b/lib/RT/Shredder.pm @@ -351,6 +351,8 @@ sub CastObjectsToRecords } elsif ( UNIVERSAL::isa( $targets, 'SCALAR' ) || !ref $targets ) { $targets = $$targets if ref $targets; my ($class, $id) = split /-/, $targets; + RT::Shredder::Exception->throw( "Unsupported class $class" ) + unless $class =~ /^\w+(::\w+)*$/; $class = 'RT::'. $class unless $class =~ /^RTx?::/i; eval "require $class"; die "Couldn't load '$class' module" if $@; diff --git a/lib/RT/Shredder/Plugin.pm b/lib/RT/Shredder/Plugin.pm index e70d207ac4..ad9af6ac6d 100644 --- a/lib/RT/Shredder/Plugin.pm +++ b/lib/RT/Shredder/Plugin.pm @@ -167,6 +167,7 @@ sub LoadByName { my $self = shift; my $name = shift or return (0, "Name not specified"); + $name =~ /^\w+(::\w+)*$/ or return (0, "Invalid plugin name"); local $@; my $plugin = "RT::Shredder::Plugin::$name"; diff --git a/lib/RT/Shredder/Queue.pm b/lib/RT/Shredder/Queue.pm index c2ec44538d..2c0d068fbc 100644 --- a/lib/RT/Shredder/Queue.pm +++ b/lib/RT/Shredder/Queue.pm @@ -91,6 +91,7 @@ sub __DependsOn # Custom Fields $objs = RT::CustomFields->new( $self->CurrentUser ); + $objs->SetContextObject( $self ); $objs->LimitToQueue( $self->id ); push( @$list, $objs ); diff --git a/lib/RT/Template.pm b/lib/RT/Template.pm index 158547a0e8..117cc3f1c5 100644 --- a/lib/RT/Template.pm +++ b/lib/RT/Template.pm @@ -96,10 +96,34 @@ sub _Accessible { sub _Set { my $self = shift; + my %args = ( + Field => undef, + Value => undef, + @_, + ); unless ( $self->CurrentUserHasQueueRight('ModifyTemplate') ) { return ( 0, $self->loc('Permission Denied') ); } + + if (exists $args{Value}) { + if ($args{Field} eq 'Queue') { + if ($args{Value}) { + # moving to another queue + my $queue = RT::Queue->new( $self->CurrentUser ); + $queue->Load($args{Value}); + unless ($queue->Id and $queue->CurrentUserHasRight('ModifyTemplate')) { + return ( 0, $self->loc('Permission Denied') ); + } + } else { + # moving to global + unless ($self->CurrentUser->HasRight( Object => RT->System, Right => 'ModifyTemplate' )) { + return ( 0, $self->loc('Permission Denied') ); + } + } + } + } + return $self->SUPER::_Set( @_ ); } diff --git a/lib/RT/Ticket.pm b/lib/RT/Ticket.pm index 3f2e94cff9..76b2e19673 100644 --- a/lib/RT/Ticket.pm +++ b/lib/RT/Ticket.pm @@ -2357,7 +2357,7 @@ sub _Links { my $links = $self->{ $cache_key } = RT::Links->new( $self->CurrentUser ); unless ( $self->CurrentUserHasRight('ShowTicket') ) { - $links->Limit( FIELD => 'id', VALUE => 0 ); + $links->Limit( FIELD => 'id', VALUE => 0, SUBCLAUSE => 'acl' ); return $links; } @@ -3513,6 +3513,16 @@ sub CurrentUserHasRight { } +=head2 CurrentUserCanSee + +Returns true if the current user can see the ticket, using ShowTicket + +=cut + +sub CurrentUserCanSee { + my $self = shift; + return $self->CurrentUserHasRight('ShowTicket'); +} =head2 HasRight @@ -3625,7 +3635,9 @@ sub Transactions { sub TransactionCustomFields { my $self = shift; - return $self->QueueObj->TicketTransactionCustomFields; + my $cfs = $self->QueueObj->TicketTransactionCustomFields; + $cfs->SetContextObject( $self ); + return $cfs; } diff --git a/lib/RT/Tickets.pm b/lib/RT/Tickets.pm index 738c3e6a37..a5fa74ea8b 100644 --- a/lib/RT/Tickets.pm +++ b/lib/RT/Tickets.pm @@ -1098,6 +1098,12 @@ sub _GroupMembersJoin { FIELD2 => 'GroupId', ENTRYAGGREGATOR => 'AND', ); + $self->SUPER::Limit( + $args{'Left'} ? (LEFTJOIN => $alias) : (), + ALIAS => $alias, + FIELD => 'Disabled', + VALUE => 0, + ); $self->{'_sql_group_members_aliases'}{ $args{'GroupsAlias'} } = $alias unless $args{'New'}; @@ -1262,6 +1268,12 @@ sub _WatcherMembershipLimit { FIELD2 => 'id' ); + $self->Limit( + ALIAS => $groupmembers, + FIELD => 'Disabled', + VALUE => 0, + ); + $self->Join( ALIAS1 => $memberships, FIELD1 => 'MemberId', @@ -1269,6 +1281,13 @@ sub _WatcherMembershipLimit { FIELD2 => 'id' ); + $self->Limit( + ALIAS => $memberships, + FIELD => 'Disabled', + VALUE => 0, + ); + + $self->_CloseParen; } @@ -1605,11 +1624,8 @@ sub _CustomFieldLimit { $self->_CloseParen; } else { - my $cf = RT::CustomField->new( $self->CurrentUser ); - $cf->Load($field); - # need special treatment for Date - if ( $cf->Type eq 'DateTime' && $op eq '=' ) { + if ( $cf and $cf->Type eq 'DateTime' and $op eq '=' ) { if ( $value =~ /:/ ) { # there is time speccified. diff --git a/lib/RT/Transaction.pm b/lib/RT/Transaction.pm index 21ccaeeccc..5b3641f1ca 100644 --- a/lib/RT/Transaction.pm +++ b/lib/RT/Transaction.pm @@ -506,7 +506,7 @@ sub Attachments { $self->{'attachments'} = RT::Attachments->new( $self->CurrentUser ); unless ( $self->CurrentUserCanSee ) { - $self->{'attachments'}->Limit(FIELD => 'id', VALUE => '0'); + $self->{'attachments'}->Limit(FIELD => 'id', VALUE => '0', SUBCLAUSE => 'acl'); return $self->{'attachments'}; } @@ -708,6 +708,7 @@ sub BriefDescription { if ( $self->Field ) { my $cf = RT::CustomField->new( $self->CurrentUser ); + $cf->SetContextObject( $self->Object ); $cf->Load( $self->Field ); $field = $cf->Name(); $field = $self->loc('a custom field') if !defined($field); @@ -1066,14 +1067,8 @@ sub CurrentUserCanSee { $cf->Load( $cf_id ); return 0 unless $cf->CurrentUserHasRight('SeeCustomField'); } - #if they ain't got rights to see, don't let em - elsif ( $self->__Value('ObjectType') eq "RT::Ticket" ) { - unless ( $self->CurrentUserHasRight('ShowTicket') ) { - return 0; - } - } - - return 1; + # Defer to the object in question + return $self->Object->CurrentUserCanSee("Transaction"); } @@ -1097,7 +1092,7 @@ sub OldValue { return $Object->Content; } else { - return $self->__Value('OldValue'); + return $self->_Value('OldValue'); } } @@ -1111,7 +1106,7 @@ sub NewValue { return $Object->Content; } else { - return $self->__Value('NewValue'); + return $self->_Value('NewValue'); } } @@ -1200,6 +1195,7 @@ sub CustomFieldValues { # do we want to cover this situation somehow here? unless ( defined $field && $field =~ /^\d+$/o ) { my $CFs = RT::CustomFields->new( $self->CurrentUser ); + $CFs->SetContextObject( $self->Object ); $CFs->Limit( FIELD => 'Name', VALUE => $field ); $CFs->LimitToLookupType($self->CustomFieldLookupType); $CFs->LimitToGlobalOrObjectId($self->Object->QueueObj->id); diff --git a/lib/RT/URI.pm b/lib/RT/URI.pm index 4af1cb0da6..fce04598a4 100644 --- a/lib/RT/URI.pm +++ b/lib/RT/URI.pm @@ -130,7 +130,7 @@ sub FromURI { # Special case: integers passed in as URIs must be ticket ids if ($uri =~ /^(\d+)$/) { $scheme = "fsck.com-rt"; - } elsif ($uri =~ /^((?:\w|\.|-)+?):/) { + } elsif ($uri =~ /^((?!javascript|data)(?:\w|\.|-)+?):/i) { $scheme = $1; } else { diff --git a/lib/RT/User.pm b/lib/RT/User.pm index 8a823774c0..00b230fbd0 100644 --- a/lib/RT/User.pm +++ b/lib/RT/User.pm @@ -1206,6 +1206,37 @@ sub HasRight { return $self->PrincipalObj->HasRight(@_); } +=head2 CurrentUserCanSee [FIELD] + +Returns true if the current user can see the user, based on if it is +public, ourself, or we have AdminUsers + +=cut + +sub CurrentUserCanSee { + my $self = shift; + my ($what) = @_; + + # If it's public, fine. Note that $what may be "transaction", which + # doesn't have an Accessible value, and thus falls through below. + if ( $self->_Accessible( $what, 'public' ) ) { + return 1; + } + + # Users can see their own properties + elsif ( defined($self->Id) && $self->CurrentUser->Id == $self->Id ) { + return 1; + } + + # If the user has the admin users right, that's also enough + elsif ( $self->CurrentUser->HasRight( Right => 'AdminUsers', Object => $RT::System) ) { + return 1; + } + else { + return 0; + } +} + =head2 CurrentUserCanModify RIGHT If the user has rights for this object, either because @@ -1334,12 +1365,13 @@ sub Stylesheet { my $style = RT->Config->Get('WebDefaultStylesheet', $self->CurrentUser); + if (RT::Interface::Web->ComponentPathIsSafe($style)) { + my @css_paths = map { $_ . '/NoAuth/css' } RT::Interface::Web->ComponentRoots; - my @css_paths = map { $_ . '/NoAuth/css' } RT::Interface::Web->ComponentRoots; - - for my $css_path (@css_paths) { - if (-d "$css_path/$style") { - return $style + for my $css_path (@css_paths) { + if (-d "$css_path/$style") { + return $style + } } } @@ -1409,6 +1441,12 @@ sub WatchedQueues { FIELD => 'MemberId', VALUE => $self->PrincipalId, ); + $watched_queues->Limit( + ALIAS => $queues_alias, + FIELD => 'Disabled', + VALUE => 0, + ); + $RT::Logger->debug("WatchedQueues got " . $watched_queues->Count . " queues"); @@ -1447,7 +1485,9 @@ sub _Set { if ( $ret == 0 ) { return ( 0, $msg ); } if ( $args{'RecordTransaction'} == 1 ) { - + if ($args{'Field'} eq "Password") { + $args{'Value'} = $Old = '********'; + } my ( $Trans, $Msg, $TransObj ) = $self->_NewTransaction( Type => $args{'TransactionType'}, Field => $args{'Field'}, @@ -1473,25 +1513,9 @@ sub _Value { my $self = shift; my $field = shift; - #if the field is public, return it. - if ( $self->_Accessible( $field, 'public' ) ) { - return ( $self->SUPER::_Value($field) ); - - } - - #If the user wants to see their own values, let them - # TODO figure ouyt a better way to deal with this - elsif ( defined($self->Id) && $self->CurrentUser->Id == $self->Id ) { - return ( $self->SUPER::_Value($field) ); - } - - #If the user has the admin users right, return the field - elsif ( $self->CurrentUser->HasRight(Right =>'AdminUsers', Object => $RT::System) ) { - return ( $self->SUPER::_Value($field) ); - } else { - return (undef); - } - + # Defer to the abstraction above to know if the field can be read + return $self->SUPER::_Value($field) if $self->CurrentUserCanSee($field); + return undef; } =head2 FriendlyName diff --git a/lib/RT/Users.pm b/lib/RT/Users.pm index 178d1dcb70..787ac10817 100644 --- a/lib/RT/Users.pm +++ b/lib/RT/Users.pm @@ -188,6 +188,9 @@ sub MemberOfGroup { FIELD1 => 'id', ALIAS2 => $groupalias, FIELD2 => 'MemberId' ); + $self->Limit( ALIAS => $groupalias, + FIELD => 'Disabled', + VALUE => 0 ); $self->Limit( ALIAS => "$groupalias", FIELD => 'GroupId', @@ -266,6 +269,11 @@ sub _JoinGroupMembers ALIAS2 => $principals, FIELD2 => 'id' ); + $self->Limit( + ALIAS => $group_members, + FIELD => 'Disabled', + VALUE => 0, + ) if $args{'IncludeSubgroupMembers'}; return $group_members; } diff --git a/sbin/rt-server.in b/sbin/rt-server.in index b438202dd8..45c3770886 100755 --- a/sbin/rt-server.in +++ b/sbin/rt-server.in @@ -91,6 +91,7 @@ if (grep { m/help/ } @ARGV) { require RT; RT->LoadConfig(); +RT->InitLogging(); require Module::Refresh if RT->Config->Get('DevelMode'); require RT::Handle; diff --git a/share/html/Admin/Articles/Elements/Topics b/share/html/Admin/Articles/Elements/Topics index 96ddaf00c7..43ca9562c7 100644 --- a/share/html/Admin/Articles/Elements/Topics +++ b/share/html/Admin/Articles/Elements/Topics @@ -105,7 +105,7 @@ $topic % } % if ($Action) { % unless ($Action eq "Move" and grep {$_->getNodeValue->Id == $Modify} $Element->getAllChildren) { -<li><input type="submit" name="<%$Prefix%>-<%$topic eq "root" ? 0 : $topic->Id%>" value="<&|/l&><%$Action%> here</&>" /></li> +<li><input type="submit" name="<%$Prefix%>-<%$topic eq "root" ? 0 : $topic->Id%>" value="<% $Action eq 'Move' ? loc('Move here') : loc('Add here') %>" /></li> % } % } </ul> diff --git a/share/html/Admin/CustomFields/Modify.html b/share/html/Admin/CustomFields/Modify.html index eec4b1f66a..1dbc47fa4c 100644 --- a/share/html/Admin/CustomFields/Modify.html +++ b/share/html/Admin/CustomFields/Modify.html @@ -105,7 +105,7 @@ <div class="hints"> <&|/l&>RT can make this custom field's values into hyperlinks to another service.</&> <&|/l&>Fill in this field with a URL.</&> -<&|/l, '<tt>__id__</tt>', '<tt>__CustomField__</tt>' &>RT will replace [_1] and [_2] with the record's id and the custom field's value, respectively.</&> +<&|/l_unsafe, '<tt>__id__</tt>', '<tt>__CustomField__</tt>' &>RT will replace [_1] and [_2] with the record's id and the custom field's value, respectively.</&> </div></td></tr> <tr><td class="label"><&|/l&>Include page</&></td><td> @@ -113,7 +113,7 @@ <div class="hints"> <&|/l&>RT can include content from another web service when showing this custom field.</&> <&|/l&>Fill in this field with a URL.</&> -<&|/l, '<tt>__id__</tt>', '<tt>__CustomField__</tt>' &>RT will replace [_1] and [_2] with the record's id and the custom field's value, respectively.</&> +<&|/l_unsafe, '<tt>__id__</tt>', '<tt>__CustomField__</tt>' &>RT will replace [_1] and [_2] with the record's id and the custom field's value, respectively.</&> <i><&|/l&>Some browsers may only load content from the same domain as your RT server.</&></i> </div></td></tr> diff --git a/share/html/Admin/Elements/EditCustomFields b/share/html/Admin/Elements/EditCustomFields index aa7b62204d..d9d9134e70 100755 --- a/share/html/Admin/Elements/EditCustomFields +++ b/share/html/Admin/Elements/EditCustomFields @@ -128,6 +128,7 @@ if ( $MoveCustomFieldDown ) { { if ( $UpdateCFs ) { foreach my $cf_id ( @AddCustomField ) { my $CF = RT::CustomField->new( $session{'CurrentUser'} ); + $CF->SetContextObject( $Object ); $CF->Load( $cf_id ); unless ( $CF->id ) { push @results, loc("Couldn't load CustomField #[_1]", $cf_id); @@ -138,6 +139,7 @@ if ( $UpdateCFs ) { } foreach my $cf_id ( @RemoveCustomField ) { my $CF = RT::CustomField->new( $session{'CurrentUser'} ); + $CF->SetContextObject( $Object ); $CF->Load( $cf_id ); unless ( $CF->id ) { push @results, loc("Couldn't load CustomField #[_1]", $cf_id); @@ -153,6 +155,7 @@ $m->callback(CallbackName => 'UpdateExtraFields', Results => \@results, Object = my $applied_cfs = RT::CustomFields->new( $session{'CurrentUser'} ); $applied_cfs->LimitToLookupType($lookup); $applied_cfs->LimitToGlobalOrObjectId($id); +$applied_cfs->SetContextObject( $Object ); $applied_cfs->ApplySortOrder; my $not_applied_cfs = RT::CustomFields->new( $session{'CurrentUser'} ); diff --git a/share/html/Admin/Elements/EditRights b/share/html/Admin/Elements/EditRights index e5b9908b56..e673593135 100644 --- a/share/html/Admin/Elements/EditRights +++ b/share/html/Admin/Elements/EditRights @@ -110,13 +110,13 @@ for my $category (@$Principals) { id="AddPrincipalForRights-<% lc $AddPrincipal %>" /> <script type="text/javascript"> jQuery(function() { - jQuery("#AddPrincipalForRights-<% lc $AddPrincipal %>").keyup(function(){ + jQuery("#AddPrincipalForRights-"+<% lc $AddPrincipal |n,j%>).keyup(function(){ toggle_addprincipal_validity(this, true); }); % if (lc $AddPrincipal eq 'group') { - jQuery("#AddPrincipalForRights-<% lc $AddPrincipal %>").autocomplete({ - source: "<% RT->Config->Get('WebPath')%>/Helpers/Autocomplete/Groups", + jQuery("#AddPrincipalForRights-"+<% lc $AddPrincipal |n,j%>).autocomplete({ + source: <% RT->Config->Get('WebPath') |n,j%>+"/Helpers/Autocomplete/Groups", select: addprincipal_onselect, change: addprincipal_onchange }); diff --git a/share/html/Admin/Elements/Portal b/share/html/Admin/Elements/Portal index d5e75c5980..821ed57287 100644 --- a/share/html/Admin/Elements/Portal +++ b/share/html/Admin/Elements/Portal @@ -47,6 +47,6 @@ %# END BPS TAGGED BLOCK }}} <div id="rt-portal"> <&| /Widgets/TitleBox, title => 'RT Portal' &> -<iframe src="http://bestpractical.com/rt/integration/news?utm_source=rt&utm_medium=iframe&utm_campaign=<%$RT::VERSION%>"></iframe> +<iframe src="https://bestpractical.com/rt/integration/news?utm_source=rt&utm_medium=iframe&utm_campaign=<%$RT::VERSION%>"></iframe> </&> </div> diff --git a/share/html/Admin/Elements/SelectNewGroupMembers b/share/html/Admin/Elements/SelectNewGroupMembers index f386ba5514..8778daec04 100755 --- a/share/html/Admin/Elements/SelectNewGroupMembers +++ b/share/html/Admin/Elements/SelectNewGroupMembers @@ -50,8 +50,8 @@ <input type="text" value="" name="<% $Name %>Users" id="<% $Name %>Users" /><br /> <script type="text/javascript"> jQuery(function(){ - jQuery("#<% $Name %>Users").autocomplete({ - source: "<% RT->Config->Get('WebPath')%>/Helpers/Autocomplete/Users?return=Name;privileged=1;exclude=<% $user_ids |u %>", + jQuery("#"+<% $Name |n,j%>+"Users").autocomplete({ + source: <% RT->Config->Get('WebPath') |n,j%>+"/Helpers/Autocomplete/Users?return=Name;privileged=1;exclude="+<% $user_ids |n,u,j %>, // Auto-submit once a user is chosen select: function( event, ui ) { jQuery(event.target).val(ui.item.value); @@ -67,8 +67,8 @@ jQuery(function(){ <input type="text" value="" name="<% $Name %>Groups" id="<% $Name %>Groups" /><br /> <script type="text/javascript"> jQuery(function(){ - jQuery("#<% $Name %>Groups").autocomplete({ - source: "<% RT->Config->Get('WebPath')%>/Helpers/Autocomplete/Groups?exclude=<% $group_ids |u %>", + jQuery("#"+<% $Name |n,j%>+"Groups").autocomplete({ + source: <% RT->Config->Get('WebPath') |n,j%>+"/Helpers/Autocomplete/Groups?exclude="+<% $group_ids |n,u,j %>, // Auto-submit once a user is chosen select: function( event, ui ) { jQuery(event.target).val(ui.item.value); diff --git a/share/html/Admin/Groups/index.html b/share/html/Admin/Groups/index.html index bd07b736a0..ef7395f3e1 100755 --- a/share/html/Admin/Groups/index.html +++ b/share/html/Admin/Groups/index.html @@ -57,7 +57,7 @@ <script type="text/javascript"> jQuery(function(){ jQuery("#autocomplete-GroupString").autocomplete({ - source: "<% RT->Config->Get('WebPath')%>/Helpers/Autocomplete/Groups", + source: <% RT->Config->Get('WebPath') |n,j%>+"/Helpers/Autocomplete/Groups", // Auto-submit once a group is chosen select: function( event, ui ) { jQuery(event.target).val(ui.item.value); diff --git a/share/html/Admin/Tools/Queries.html b/share/html/Admin/Tools/Queries.html index 2fe2d5ac1e..dbc6fc5fe4 100644 --- a/share/html/Admin/Tools/Queries.html +++ b/share/html/Admin/Tools/Queries.html @@ -79,7 +79,7 @@ unless ($session{'CurrentUser'}->HasRight( Object=> $RT::System, Right => 'Super <li> <tt><% $request->{Path} %></tt> - <i><&|/l, sprintf('%.4f', $seconds) &>[_1]s</&></i> - <a href="#" onclick="return hideshow('queries-<%$r%>');"><&|/l, $count &>Toggle [quant,_1,query,queries]</&></a> + <a href="#" onclick="return hideshow(<% "queries-$r" |n,j%>);"><&|/l, $count &>Toggle [quant,_1,query,queries]</&></a> <table id="queries-<%$r%>" class="tablesorter hidden"> <thead> <tr> @@ -115,7 +115,7 @@ unless ($session{'CurrentUser'}->HasRight( Object=> $RT::System, Right => 'Super <br><tt>[<% join(", ", @$b) %>]</tt> % } % } - <a class="query-stacktrace-toggle" href="#" onclick="return hideshow('trace-<%$r%>-<%$s%>');"><&|/l &>Toggle stack trace</&></a> + <a class="query-stacktrace-toggle" href="#" onclick="return hideshow(<% "trace-$r-$s" |n,j%>);"><&|/l &>Toggle stack trace</&></a> <pre id="trace-<%$r%>-<%$s%>" class="hidden"><% $trace %></pre> </td> </tr> diff --git a/share/html/Admin/Tools/Shredder/Dumps/dhandler b/share/html/Admin/Tools/Shredder/Dumps/dhandler index 8b84cf4d9b..0d24fa0af7 100644 --- a/share/html/Admin/Tools/Shredder/Dumps/dhandler +++ b/share/html/Admin/Tools/Shredder/Dumps/dhandler @@ -48,9 +48,6 @@ <%ATTR> AutoFlush => 0 </%ATTR> -<%FLAGS> -inherit => undef -</%FLAGS> <%INIT> my $arg = $m->dhandler_arg; $m->abort(404) if $arg =~ m{\.\.|/|\\}; @@ -64,5 +61,5 @@ my $buf; while( read $fh, $buf, 1024*1024 ) { $m->out($buf); } -return 0; +$m->abort; </%INIT> diff --git a/share/html/Admin/Tools/Shredder/Elements/Error/NoStorage b/share/html/Admin/Tools/Shredder/Elements/Error/NoStorage index ce934111cf..ae3b96e9b0 100644 --- a/share/html/Admin/Tools/Shredder/Elements/Error/NoStorage +++ b/share/html/Admin/Tools/Shredder/Elements/Error/NoStorage @@ -52,5 +52,5 @@ $Path => '' <& /Elements/Tabs &> <div class="error"> % my $path_tag = q{<span class="file-path">} . $m->interp->apply_escapes($Path, 'h') . q{</span>}; -<&|/l, $path_tag &>Shredder needs a directory to write dumps to. Please ensure that the directory [_1] exists and that it is writable by your web server.</&> +<&|/l_unsafe, $path_tag &>Shredder needs a directory to write dumps to. Please ensure that the directory [_1] exists and that it is writable by your web server.</&> </div> diff --git a/share/html/Admin/Users/index.html b/share/html/Admin/Users/index.html index a1e3facd71..adcfeb5b90 100755 --- a/share/html/Admin/Users/index.html +++ b/share/html/Admin/Users/index.html @@ -62,7 +62,7 @@ <script type="text/javascript"> jQuery(function(){ jQuery("#autocomplete-UserString").autocomplete({ - source: "<% RT->Config->Get('WebPath')%>/Helpers/Autocomplete/Users?return=Name", + source: <% RT->Config->Get('WebPath') |n,j%>+"/Helpers/Autocomplete/Users?return=Name", // Auto-submit once a user is chosen select: function( event, ui ) { jQuery(event.target).val(ui.item.value); diff --git a/share/html/Approvals/Elements/PendingMyApproval b/share/html/Approvals/Elements/PendingMyApproval index 75ad5e1ab9..d2061da84d 100755 --- a/share/html/Approvals/Elements/PendingMyApproval +++ b/share/html/Approvals/Elements/PendingMyApproval @@ -63,9 +63,9 @@ <input type="checkbox" class="checkbox" value="1" name="ShowRejected" <% defined($ARGS{'ShowRejected'}) && $ARGS{'ShowRejected'} && qq[checked="checked"] |n%> /> <&|/l&>Show denied requests</&><br /> <input type="checkbox" class="checkbox" value="1" name="ShowDependent" <% defined($ARGS{'ShowDependent'}) && $ARGS{'ShowDependent'} && qq[checked="checked"] |n%> /> <&|/l&>Show requests awaiting other approvals</&><br /> -<&|/l, qq{<input size='15' class="ui-datepicker" value='}.($created_before->Unix > 0 &&$created_before->ISO(Timezone => 'user'))."' name='CreatedBefore' id='CreatedBefore' />"&>Only show approvals for requests created before [_1]</&><br /> +<&|/l_unsafe, qq{<input size='15' class="ui-datepicker" value='}.($created_before->Unix > 0 &&$created_before->ISO(Timezone => 'user'))."' name='CreatedBefore' id='CreatedBefore' />"&>Only show approvals for requests created before [_1]</&><br /> -<&|/l, qq{<input size='15' class="ui-datepicker" value='}.( $created_after->Unix >0 && $created_after->ISO(Timezone => 'user'))."' name='CreatedAfter' id='CreatedAfter' />"&>Only show approvals for requests created after [_1]</&> +<&|/l_unsafe, qq{<input size='15' class="ui-datepicker" value='}.( $created_after->Unix >0 && $created_after->ISO(Timezone => 'user'))."' name='CreatedAfter' id='CreatedAfter' />"&>Only show approvals for requests created after [_1]</&> </&> <%init> diff --git a/share/html/Articles/Article/Edit.html b/share/html/Articles/Article/Edit.html index 756aa2cc97..d14c330760 100644 --- a/share/html/Articles/Article/Edit.html +++ b/share/html/Articles/Article/Edit.html @@ -157,6 +157,7 @@ elsif ( $id eq 'new' ) { my $cfid = $1; my $cf = RT::CustomField->new( $session{'CurrentUser'} ); + $cf->SetContextObject( $ArticleObj ); $cf->Load( $cfid ); unless ( $cf->id ) { $RT::Logger->error( "Couldn't load custom field #". $cfid ); diff --git a/share/html/Articles/Article/Elements/EditTopics b/share/html/Articles/Article/Elements/EditTopics index 807360bf2a..82e907135b 100644 --- a/share/html/Articles/Article/Elements/EditTopics +++ b/share/html/Articles/Article/Elements/EditTopics @@ -47,35 +47,32 @@ %# END BPS TAGGED BLOCK }}} <input type="hidden" name="EditTopics" value="1" /> <select multiple size="10" name="Topics"> -<%perl> -if (@Classes) { - $m->print("<optgroup label=\"Current classes (".join (' ',map {$_->Name} @Classes).")\">") - unless $OnlyThisClass; - $inTree->traverse(sub { - my $tree = shift; - my $topic = $tree->getNodeValue; - $m->print("<option value=\"".$topic->Id."\"" - .(exists $topics{$topic->Id} ? " selected" : "").">" - .(" " x ($tree->getDepth*5)).($topic->Name || loc("(no name)"))."</option>\n"); - }); -} -unless ($OnlyThisClass) { - my $class = $Classes[-1]->Id; - $otherTree->traverse(sub { - my $tree = shift; - my $topic = $tree->getNodeValue; - unless ($topic->ObjectId == $class) { - $class = $topic->ObjectId; - $m->print("</optgroup>\n"); - my $c = RT::Class->new($session{'CurrentUser'}); - $c->Load($topic->ObjectId); - $m->print("<optgroup label=\"".$c->Name."\">\n"); - } - $m->print("<option value=\"".$topic->Id."\"" - .(exists $topics{$topic->Id} ? " selected" : "").">" - .(" " x ($tree->getDepth*5)).($topic->Name || loc("(no name)"))."</option>\n"); - }); -</%perl> +% if (@Classes) { +% unless ($OnlyThisClass) { +<optgroup label="Current classes (<% join(" ", map {$_->Name} @Classes) %>)"> +% } +% $inTree->traverse(sub { +% my $tree = shift; +% my $topic = $tree->getNodeValue; +<option value="<% $topic->Id %>" <% exists $topics{$topic->Id} ? "selected" : "" %> >\ +<% " " x ($tree->getDepth*5) |n %><% $topic->Name || loc("(no name)") %></option> +% }); +% } +% unless ($OnlyThisClass) { +% my $class = $Classes[-1]->Id; +% $otherTree->traverse(sub { +% my $tree = shift; +% my $topic = $tree->getNodeValue; +% unless ($topic->ObjectId == $class) { +% $class = $topic->ObjectId; +</optgroup> +% my $c = RT::Class->new($session{'CurrentUser'}); +% $c->Load($topic->ObjectId); +<optgroup label="<% $c->Name %>"> +% } +<option value="<% $topic->Id %>" <% exists $topics{$topic->Id} ? "selected" : "" %> >\ +<% " " x ($tree->getDepth*5) |n %><% $topic->Name || loc("(no name)") %></option> +% }); </optgroup> % } </select> diff --git a/share/html/Articles/Article/ExtractIntoClass.html b/share/html/Articles/Article/ExtractIntoClass.html index adf23fc0f6..f3618fedae 100644 --- a/share/html/Articles/Article/ExtractIntoClass.html +++ b/share/html/Articles/Article/ExtractIntoClass.html @@ -54,7 +54,7 @@ % my $Classes = RT::Classes->new($session{'CurrentUser'}); % $Classes->LimitToEnabled(); % while (my $Class = $Classes->Next) { -<li><a href="ExtractIntoTopic.html?Ticket=<%$Ticket%>&Class=<%$Class->Id%>" onclick="document.getElementById('topics-<% $Class->Id %>').style.display = (document.getElementById('topics-<% $Class->Id %>').style.display == 'block') ? 'none' : 'block'; return false;"><%$Class->Name%></a>: +<li><a href="ExtractIntoTopic.html?Ticket=<%$Ticket%>&Class=<%$Class->Id%>" onclick="document.getElementById('topics-'+<% $Class->Id |n,j%>).style.display = (document.getElementById('topics-'+<% $Class->Id |n,j%>).style.display == 'block') ? 'none' : 'block'; return false;"><%$Class->Name%></a>: <%$Class->Description%> <div id="topics-<%$Class->Id%>" style="display: none"> <form action="ExtractFromTicket.html"> diff --git a/share/html/Articles/Elements/ShowTopicLink b/share/html/Articles/Elements/ShowTopicLink new file mode 100644 index 0000000000..7b6d550be7 --- /dev/null +++ b/share/html/Articles/Elements/ShowTopicLink @@ -0,0 +1,27 @@ +<%args> +$Topic +$Class => 0 +</%args> +% if ($Link) { +<a href="Topics.html?id=<% $Topic->Id %>&class=<% $Class %>">\ +% } +<% $Topic->Name() || loc("(no name)") %>\ +% if ($Topic->Description) { +: <% $Topic->Description %> +% } + +% if ( $Articles->Count ) { + (<&|/l, $Articles->Count &>[quant,_1,article]</&>) +% } + +% if ($Link) { +</a> +% } + +<%init> +my $Articles = RT::ObjectTopics->new( $session{'CurrentUser'} ); +$Articles->Limit( FIELD => 'ObjectType', VALUE => 'RT::Article' ); +$Articles->Limit( FIELD => 'Topic', VALUE => $Topic->Id ); + +my $Link = $Topic->Children->Count || $Articles->Count; +</%init> diff --git a/share/html/Articles/Topics.html b/share/html/Articles/Topics.html index 9a07c089ad..5187315f7a 100644 --- a/share/html/Articles/Topics.html +++ b/share/html/Articles/Topics.html @@ -48,7 +48,6 @@ <& /Elements/Header, Title => loc('Browse by topic') &> <& /Elements/Tabs &> -<& /Elements/ListActions, actions => \@Actions &> <a href="Topics.html"><&|/l&>All topics</&></a> % if (defined $class) { > <a href="Topics.html?class=<%$currclass_id%>"><% $currclass_name %></a> @@ -59,71 +58,41 @@ % } <br /> <h1><&|/l&>Browse by topic</&></h1> -<%perl> -if (defined $class) { - $m->print('<h2>'.'<a href="'. - RT->Config->Get('WebPath')."/Articles/Topics.html?class=" . $currclass_id - .'">'.$currclass_name."</a></h2>\n"); - ProduceTree(\@Actions, $currclass, $currclass_id, $currclass_name, 0, $id); -} else { - $m->print("<ul>\n"); - while (my $c = $Classes->Next) { - $m->print('<li><h2>'.'<a href="'. - RT->Config->Get('WebPath')."/Articles/Topics.html?class=" . $c->Id - .'">'.$c->Name."</a></h2>\n"); - $m->print("\n</li>\n"); - } - $m->print(qq|<li><h2><a href="|.RT->Config->Get('WebPath').qq|/Articles/Topics.html?class=0">|.loc('Global Topics').qq|</a></h2></li>\n|); - $m->print("</ul>\n"); -} -</%perl> +% if (defined $class) { +<h2><a href="<% RT->Config->Get('WebPath') %>/Articles/Topics.html?class=<% $currclass_id %>"><% $currclass_name %></a></h2> +% my $rtopic = RT::Topic->new( $session{'CurrentUser'} ); +% $rtopic->Load($id); +% unless ( $rtopic->Id() +% && $rtopic->ObjectId() == $currclass->Id ) +% { +% # Show all of them +% $ProduceTree->( 0 ); +% } else { +% my @showtopics = ( $rtopic ); +% my $parent = $rtopic->ParentObj; +% while ( $parent->Id ) { +% unshift @showtopics, $parent; +% $parent = $parent->ParentObj; +% } +% # List the topics. +% for my $t ( @showtopics ) { +<ul><li><& /Articles/Elements/ShowTopicLink, Topic => $t, Class => $currclass_id &> +% $ProduceTree->( $id ) if $t->Id == $id; +% } +% for ( @showtopics ) { + </li></ul> +% } +% } +% } else { +<ul> +% while (my $c = $Classes->Next) { +<li><h2><a href="<% RT->Config->Get('WebPath') %>/Articles/Topics.html?class=<% $c->Id %>"><% $c->Name %></a></h2></li> +% } +<li><h2><a href="<% RT->Config->Get('WebPath') %>/Articles/Topics.html?class=0"><&|/l&>Global Topics</&></a></h2></li> +</ul> +% } <br /> -<%perl> -my @articles; -if ($id or $showall) { - my $Articles = RT::ObjectTopics->new($session{'CurrentUser'}); - $Articles->Limit(FIELD => 'ObjectType', VALUE => 'RT::Article'); - if ($id) { - $Articles->Limit(FIELD => 'Topic', VALUE => $id, ENTRYAGGREGATOR => 'OR'); - if ($showall) { - my $kids = $currtopic->Children; - while (my $k = $kids->Next) { - $Articles->Limit(FIELD => 'Topic', VALUE => $k->Id, - ENTRYAGGREGATOR => 'OR'); - } - } - } - @articles = map {$a = RT::Article->new($session{'CurrentUser'}); $a->Load($_->ObjectId); $a} @{$Articles->ItemsArrayRef} -} elsif ($class) { - my $Articles = RT::Articles->new($session{'CurrentUser'}); - my $TopicsAlias = $Articles->Join( - TYPE => 'left', - ALIAS1 => 'main', - FIELD1 => 'id', - TABLE2 => 'ObjectTopics', - FIELD2 => 'ObjectId', - ); - $Articles->Limit( - LEFTJOIN => $TopicsAlias, - FIELD => 'ObjectType', - VALUE => 'RT::Article', - ); - $Articles->Limit( - ALIAS => $TopicsAlias, - FIELD => 'Topic', - OPERATOR => 'IS', - VALUE => 'NULL', - QUOTEVALUE => 0, - ); - $Articles->Limit( - FIELD => 'Class', - OPERATOR => '=', - VALUE => $class, - ); - @articles = @{$Articles->ItemsArrayRef}; -} -</%perl> % if (@articles) { % if ($id) { @@ -139,7 +108,6 @@ if ($id or $showall) { % } <%init> -my @Actions; my $Classes; my $currclass; my $currclass_id; @@ -167,106 +135,65 @@ if ($id) { $currtopic->Load($id); } -# A subroutine that iterates through topics and their children, producing -# the necessary ul, li, and href links for the table of contents. Thank -# heaven for query caching. The $restrict variable is used to display only -# the branch of the hierarchy which contains that topic ID. - -sub ProduceTree { - my ( $Actions, $currclass, $currclass_id, $currclass_name, $parentid, $restrictid ) = @_; - $parentid = 0 unless $parentid; - - # Deal with tree restriction, if any. - if ($restrictid) { - my $rtopic = RT::Topic->new( $session{'CurrentUser'} ); - $rtopic->Load($restrictid); - unless ( $rtopic->Id() - && $rtopic->ObjectId() == $currclass_id ) - { - push( @{$Actions},"Could not restrict view to topic $restrictid"); - - # Start over, without the restriction. - &ProduceTree( $Actions, $currclass, $currclass_id, $currclass_name, $parentid, undef ); - } else { - my @showtopics; - push( @showtopics, $rtopic ); - my $parent = $rtopic->ParentObj; - while ( $parent->Id ) { - push( @showtopics, $parent ); - my $newparent = $parent->ParentObj; - $parent = $newparent; - } - - # List the topics. - my $indents = @showtopics; - while ( my $t = pop @showtopics ) { - print "<ul>"; - print &MakeLinks( $t, $currclass, $currclass_id, $currclass_name, $t->Children->Count ); - if ( $t->Id == $restrictid ) { - &ProduceTree( $Actions, $currclass, $currclass_id, $currclass_name, $restrictid, undef ); - } - } - print "</ul>" x $indents; - } - } else { - - # No restriction in place. Build the entire tree. - my $topics = RT::Topics->new( $session{'CurrentUser'} ); - $topics->LimitToObject($currclass); - $topics->LimitToKids($parentid); - $topics->OrderBy( FIELD => 'Name' ); - print "<ul>" if $topics->Count; - while ( my $t = $topics->Next ) { - if ( $t->Children->Count ) { - print &MakeLinks( $t, $currclass, $currclass_id, $currclass_name, 1 ); - &ProduceTree( $Actions, $currclass, $currclass_id, $currclass_name, $t->Id ); - } else { - print &MakeLinks( $t, $currclass, $currclass_id, $currclass_name, 0 ); - } - } - print "</ul>\n" if $topics->Count; +my $ProduceTree; +$ProduceTree = sub { + my ( $parentid ) = @_; + my $topics = RT::Topics->new( $session{'CurrentUser'} ); + $topics->LimitToObject($currclass); + $topics->LimitToKids($parentid || 0); + $topics->OrderBy( FIELD => 'Name' ); + return unless $topics->Count; + $m->out("<ul>"); + while ( my $t = $topics->Next ) { + $m->out("<li>"); + $m->comp("/Articles/Elements/ShowTopicLink", + Topic => $t, + Class => $currclass_id, + ); + $ProduceTree->( $t->Id ) if $t->Children->Count; + $m->out("</li>"); } -} - -sub MakeLinks { - my ( $topic, $currclass, $currclass_id, $currclass_name, $haschild ) = @_; - my $query; - my $output; - - if ( ref($topic) eq 'RT::Topic' ) { - - my $topic_info = $topic->Name() || loc("(no name)"); - $topic_info .= ": " . $topic->Description() if $topic->Description; - - if ($haschild) { # has topics below it - $query = "Topics.html?id=" . $topic->Id . "&class=" . $currclass_id; - $output = qq(<li><a href="$query">$topic_info</a>); - } else { - $output = qq(<li>$topic_info); - } + $m->out("</ul>"); +}; - my $Articles = RT::ObjectTopics->new( $session{'CurrentUser'} ); - $Articles->Limit( FIELD => 'ObjectType', VALUE => 'RT::Article' ); - $Articles->Limit( FIELD => 'Topic', VALUE => $topic->Id ); - if ( $Articles->Count ) { - my $article_text = " (" . loc( "[quant,_1,article]", $Articles->Count ) . ")"; - my $query = "Topics.html?id=" . $topic->Id . "&class=$currclass_id&showall=1"; - $output .= qq(<a href="$query">$article_text</a>); - } - - $output .= "</li>\n"; - - } else { - - # This builds a link for the class specified, with no particular topic. - $query = "Topics.html?class=" . $currclass_id; - $output = "<li><a href=\"$query\">" . $currclass_name . "</a>"; - $output .= ": " . $currclass->Description if $currclass->Description; +my @articles; +if ($id) { + my $Articles = RT::ObjectTopics->new($session{'CurrentUser'}); + $Articles->Limit(FIELD => 'ObjectType', VALUE => 'RT::Article'); + $Articles->Limit(FIELD => 'Topic', VALUE => $id); + while (my $objtopic = $Articles->Next) { + my $a = RT::Article->new($session{'CurrentUser'}); + $a->Load($objtopic->ObjectId); + push @articles, $a; } - - return $output; +} elsif ($class) { + my $Articles = RT::Articles->new($session{'CurrentUser'}); + my $TopicsAlias = $Articles->Join( + TYPE => 'left', + ALIAS1 => 'main', + FIELD1 => 'id', + TABLE2 => 'ObjectTopics', + FIELD2 => 'ObjectId', + ); + $Articles->Limit( + LEFTJOIN => $TopicsAlias, + FIELD => 'ObjectType', + VALUE => 'RT::Article', + ); + $Articles->Limit( + ALIAS => $TopicsAlias, + FIELD => 'Topic', + OPERATOR => 'IS', + VALUE => 'NULL', + QUOTEVALUE => 0, + ); + $Articles->Limit( + FIELD => 'Class', + OPERATOR => '=', + VALUE => $class, + ); + @articles = @{$Articles->ItemsArrayRef}; } - </%init> <%args> diff --git a/share/html/Elements/CSRF b/share/html/Elements/CSRF new file mode 100644 index 0000000000..4893c1216c --- /dev/null +++ b/share/html/Elements/CSRF @@ -0,0 +1,74 @@ +%# BEGIN BPS TAGGED BLOCK {{{ +%# +%# COPYRIGHT: +%# +%# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +%# <sales@bestpractical.com> +%# +%# (Except where explicitly superseded by other copyright notices) +%# +%# +%# LICENSE: +%# +%# This work is made available to you under the terms of Version 2 of +%# the GNU General Public License. A copy of that license should have +%# been provided with this software, but in any event can be snarfed +%# from www.gnu.org. +%# +%# This work is distributed in the hope that it will be useful, but +%# WITHOUT ANY WARRANTY; without even the implied warranty of +%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%# General Public License for more details. +%# +%# You should have received a copy of the GNU General Public License +%# along with this program; if not, write to the Free Software +%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +%# 02110-1301 or visit their web page on the internet at +%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +%# +%# +%# CONTRIBUTION SUBMISSION POLICY: +%# +%# (The following paragraph is not intended to limit the rights granted +%# to you to modify and distribute this software under the terms of +%# the GNU General Public License and is only of importance to you if +%# you choose to contribute your changes and enhancements to the +%# community by submitting them to Best Practical Solutions, LLC.) +%# +%# By intentionally submitting any modifications, corrections or +%# derivatives to this work, or any other work intended for use with +%# Request Tracker, to Best Practical Solutions, LLC, you confirm that +%# you are the copyright holder for those contributions and you grant +%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +%# royalty-free, perpetual, license to use, copy, create derivative +%# works based on those contributions, and sublicense and distribute +%# those contributions and any derivatives thereof. +%# +%# END BPS TAGGED BLOCK }}} +<& /Elements/Header, Title => loc('Possible cross-site request forgery') &> +<& /Elements/Tabs &> + +<h1><&|/l&>Possible cross-site request forgery</&></h1> + +% my $strong_start = "<strong>"; +% my $strong_end = "</strong>"; +<p><&|/l_unsafe, $strong_start, $strong_end, $Reason &>RT has detected a possible [_1]cross-site request forgery[_2] for this request, because [_3]. This is possibly caused by a malicious attacker trying to perform actions against RT on your behalf. If you did not initiate this request, then you should alert your security team.</&></p> + +% my $start = qq|<strong><a href="$url_with_token">|; +% my $end = qq|</a></strong>|; +<p><&|/l_unsafe, $escaped_path, $start, $end &>If you really intended to visit [_1], then [_2]click here to resume your request[_3].</&></p> + +<& /Elements/Footer, %ARGS &> +% $m->abort; +<%ARGS> +$OriginalURL => '' +$Reason => '' +$Token => '' +</%ARGS> +<%INIT> +my $escaped_path = $m->interp->apply_escapes($OriginalURL, 'h'); +$escaped_path = "<tt>$escaped_path</tt>"; + +my $url_with_token = URI->new($OriginalURL); +$url_with_token->query_form([CSRF_Token => $Token]); +</%INIT> diff --git a/share/html/Elements/CollectionAsTable/Header b/share/html/Elements/CollectionAsTable/Header index 81d8bbb27a..20586f9c3e 100644 --- a/share/html/Elements/CollectionAsTable/Header +++ b/share/html/Elements/CollectionAsTable/Header @@ -129,11 +129,11 @@ foreach my $col ( @Format ) { if $OrderBy[0] && ($OrderBy[0] eq $attr or "$attr|$OrderBy[0]" =~ /^(Created|id)\|(Created|id)$/); $m->out( - '<a href="' . $BaseURL + '<a href="' . $m->interp->apply_escapes($BaseURL . $m->comp( '/Elements/QueryString', %$generic_query_args, OrderBy => $attr, Order => $new_order - ) + ), 'h') . '">'. loc($title) .'</a>' ); } diff --git a/share/html/Elements/CollectionListPaging b/share/html/Elements/CollectionListPaging index b1faa21015..26c0823480 100644 --- a/share/html/Elements/CollectionListPaging +++ b/share/html/Elements/CollectionListPaging @@ -55,22 +55,24 @@ $URLParams => undef </%ARGS> <%INIT> +$BaseURL = $m->interp->apply_escapes($BaseURL, 'h'); + $m->out(qq{<div class="paging">}); if ($Pages == 1) { $m->out(loc('Page 1 of 1')); } else{ $m->out(loc('Page') . ' '); -my $prev = $m->comp( +my $prev = $m->interp->apply_escapes($m->comp( '/Elements/QueryString', %$URLParams, Page => ( $CurrentPage - 1 ) - ); -my $next = $m->comp( + ), 'h'); +my $next = $m->interp->apply_escapes($m->comp( '/Elements/QueryString', %$URLParams, Page => ( $CurrentPage + 1 ) - ); + ), 'h'); my %show; $show{1} = 1; $show{$_} = 1 for (($CurrentPage - 2)..($CurrentPage + 2)); @@ -81,7 +83,7 @@ for my $number ( 1 .. $Pages ) { if ( $show{$number} ) { $dots = undef; my $qs = - $m->comp( '/Elements/QueryString', %$URLParams, Page => $number ); + $m->interp->apply_escapes($m->comp( '/Elements/QueryString', %$URLParams, Page => $number ), 'h'); $m->out(qq{<span class="pagenum">}); if ( $number == $CurrentPage ) { $m->out(qq{<span class="currentpage">$number</span> }); diff --git a/share/html/Elements/ColumnMap b/share/html/Elements/ColumnMap index f2fe434c70..87fd61b126 100644 --- a/share/html/Elements/ColumnMap +++ b/share/html/Elements/ColumnMap @@ -118,14 +118,16 @@ my $COLUMN_MAP = { my $name = $_[1] || 'SelectedTickets'; my $checked = $m->request_args->{ $name .'All' }? 'checked="checked"': ''; - return \qq{<input type="checkbox" name="${name}All" value="1" $checked - onclick="setCheckbox(this.form, '$name', this.checked)" />}; + return \qq{<input type="checkbox" name="}, $name, \qq{All" value="1" $checked + onclick="setCheckbox(this.form, }, + $m->interp->apply_escapes($name,'j'), + \qq{, this.checked)" />}; }, value => sub { my $id = $_[0]->id; my $name = $_[2] || 'SelectedTickets'; - return \qq{<input type="checkbox" name="$name" value="$id" checked="checked" />} + return \qq{<input type="checkbox" name="}, $name, \qq{" value="$id" checked="checked" />} if $m->request_args->{ $name . 'All'}; my $arg = $m->request_args->{ $name }; @@ -136,7 +138,7 @@ my $COLUMN_MAP = { elsif ( $arg ) { $checked = 'checked="checked"' if $arg == $id; } - return \qq{<input type="checkbox" name="$name" value="$id" $checked />} + return \qq{<input type="checkbox" name="}, $name, \qq{" value="$id" $checked />} }, }, RadioButton => { diff --git a/share/html/Elements/CreateTicket b/share/html/Elements/CreateTicket index 6e541db9b2..6702abcbfc 100755 --- a/share/html/Elements/CreateTicket +++ b/share/html/Elements/CreateTicket @@ -51,7 +51,7 @@ % my $button_start = '<input type="submit" class="button" value="'; % my $button_end = '" />'; % my $queue_selector = $m->scomp('/Elements/SelectNewTicketQueue', OnChange => 'document.CreateTicketInQueue.submit()', SendTo => $SendTo ); -<&|/l, $button_start, $button_end, $queue_selector &>[_1]New ticket in[_2] [_3]</&> +<&|/l_unsafe, $button_start, $button_end, $queue_selector &>[_1]New ticket in[_2] [_3]</&> % $m->callback(CallbackName => 'BeforeFormEnd'); </form> <%ARGS> diff --git a/share/html/Elements/EditCustomField b/share/html/Elements/EditCustomField index c7c8bfa367..b74c4844e5 100644 --- a/share/html/Elements/EditCustomField +++ b/share/html/Elements/EditCustomField @@ -85,7 +85,7 @@ if ($MaxValues == 1 && $Values) { } # The "Magic" hidden input causes RT to know that we were trying to edit the field, even if # we don't see a value later, since browsers aren't compelled to submit empty form fields -$m->out("\n".'<input type="hidden" class="hidden" name="'.$NamePrefix.$CustomField->Id.'-Values-Magic" value="1" />'."\n"); +$m->out("\n".'<input type="hidden" class="hidden" name="'.$m->interp->apply_escapes($NamePrefix, 'h').$CustomField->Id.'-Values-Magic" value="1" />'."\n"); my $EditComponent = "EditCustomField$Type"; $m->callback( %ARGS, CallbackName => 'EditComponentName', Name => \$EditComponent, CustomField => $CustomField, Object => $Object ); diff --git a/share/html/Elements/EditCustomFieldAutocomplete b/share/html/Elements/EditCustomFieldAutocomplete index aaf551716f..911e60707d 100644 --- a/share/html/Elements/EditCustomFieldAutocomplete +++ b/share/html/Elements/EditCustomFieldAutocomplete @@ -49,10 +49,10 @@ <textarea cols="<% $Cols %>" rows="<% $Rows %>" name="<% $name %>-Values" id="<% $name %>-Values" class="CF-<%$CustomField->id%>-Edit"><% $Default || '' %></textarea> <script type="text/javascript"> -var id = '<% $name . '-Values' %>'; +var id = <% "$name-Values" |n,j%>; id = id.replace(/:/g,'\\:'); jQuery('#'+id).autocomplete( { - source: "<%RT->Config->Get('WebPath')%>/Helpers/Autocomplete/CustomFieldValues?<% $name . '-Values' %>", + source: <%RT->Config->Get('WebPath') |n,j%>+"/Helpers/Autocomplete/CustomFieldValues?"+<% $Context |n,j %>+<% "$name-Values" |n,u,j%>, focus: function () { // prevent value inserted on focus return false; @@ -73,10 +73,10 @@ jQuery('#'+id).autocomplete( { % } else { <input type="text" id="<% $name %>-Value" name="<% $name %>-Value" class="CF-<%$CustomField->id%>-Edit" value="<% $Default || '' %>"/> <script type="text/javascript"> -var id = '<% $name . '-Value' %>'; +var id = <% "$name-Value" |n,j%>; id = id.replace(/:/g,'\\:'); jQuery('#'+id).autocomplete( { - source: "<%RT->Config->Get('WebPath')%>/Helpers/Autocomplete/CustomFieldValues?<% $name . '-Value' %>" + source: <%RT->Config->Get('WebPath')|n,j%>+"/Helpers/Autocomplete/CustomFieldValues?"+<% $Context |n,j %>+<% "$name-Value" |n,u,j%> } ); % } @@ -92,6 +92,11 @@ if ( $Multiple and $Values ) { $Default .= $value->Content ."\n"; } } +my $Context = ""; +if ($CustomField->ContextObject) { + $Context .= "ContextId=" . $CustomField->ContextObject->Id . "&"; + $Context .= "ContextType=". ref($CustomField->ContextObject) . "&"; +} </%INIT> <%ARGS> $CustomField => undef diff --git a/share/html/Elements/EditCustomFieldSelect b/share/html/Elements/EditCustomFieldSelect index b3fefbd49a..ed6bb146ff 100644 --- a/share/html/Elements/EditCustomFieldSelect +++ b/share/html/Elements/EditCustomFieldSelect @@ -55,7 +55,7 @@ % if (!$HideCategory and @category and not $CustomField->BasedOnObj->id) { <script type="text/javascript" src="<%RT->Config->Get('WebPath')%>/NoAuth/js/cascaded.js"></script> %# XXX - Hide this select from w3m? - <select onchange="filter_cascade('<% $id %>-Values', this.value)" name="<% $id %>-Category" class="CF-<%$CustomField->id%>-Edit"> + <select onchange="filter_cascade(<% "$id-Values" |n,j%>, this.value)" name="<% $id %>-Category" class="CF-<%$CustomField->id%>-Edit"> <option value=""<% !$selected && qq[ selected="selected"] |n %>><&|/l&>-</&></option> % foreach my $cat (@category) { % my ($depth, $name) = @$cat; @@ -66,12 +66,12 @@ <script type="text/javascript" src="<%RT->Config->Get('WebPath')%>/NoAuth/js/cascaded.js"></script> <script type="text/javascript"><!-- jQuery( function () { - var basedon = document.getElementById('<% $NamePrefix . $CustomField->BasedOnObj->id %>-Values'); + var basedon = document.getElementById(<% $NamePrefix . $CustomField->BasedOnObj->id . "-Values" |n,j%>); if (basedon != null) { var oldchange = basedon.onchange; basedon.onchange = function () { filter_cascade( - '<% $id %>-Values', + <% "$id-Values" |n,j%>, basedon.value, 1 ); diff --git a/share/html/Elements/Error b/share/html/Elements/Error index 50f3b775c5..87dfd0245d 100755 --- a/share/html/Elements/Error +++ b/share/html/Elements/Error @@ -81,7 +81,7 @@ Encode::_utf8_off($error); $RT::Logger->error($error); -if ( defined $session{'SessionType'} && $session{'SessionType'} eq 'REST' ) { +if ( $session{'REST'} ) { $r->content_type('text/plain'); $m->out( "Error: " . $Why . "\n" ); $m->out( $Details . "\n" ) if defined $Details && length $Details; diff --git a/share/html/Elements/Footer b/share/html/Elements/Footer index cbe8a0e7bf..433a691e36 100755 --- a/share/html/Elements/Footer +++ b/share/html/Elements/Footer @@ -53,10 +53,10 @@ % if ($m->{'rt_base_time'}) { <p id="time"><span><&|/l&>Time to display</&>: <%Time::HiRes::tv_interval( $m->{'rt_base_time'} )%></span></p> %} - <p id="bpscredits"><span><&|/l, '»|«', $RT::VERSION, '2012', '<a href="http://www.bestpractical.com?rt='.$RT::VERSION.'">Best Practical Solutions, LLC</a>', &>[_1] RT [_2] Copyright 1996-[_3] [_4].</&> + <p id="bpscredits"><span><&|/l_unsafe, '»|«', $RT::VERSION, '2012', '<a href="http://www.bestpractical.com?rt='.$RT::VERSION.'">Best Practical Solutions, LLC</a>', &>[_1] RT [_2] Copyright 1996-[_3] [_4].</&> </span></p> % if (!$Menu) { - <p id="legal"><&|/l, '<a href="http://www.gnu.org/licenses/gpl-2.0.html">', '</a>' &>Distributed under [_1]version 2 of the GNU GPL[_2].</&><br /><&|/l, '<a href="mailto:sales@bestpractical.com">sales@bestpractical.com</a>' &>To inquire about support, training, custom development or licensing, please contact [_1].</&><br /></p> + <p id="legal"><&|/l_unsafe, '<a href="http://www.gnu.org/licenses/gpl-2.0.html">', '</a>' &>Distributed under [_1]version 2 of the GNU GPL[_2].</&><br /><&|/l_unsafe, '<a href="mailto:sales@bestpractical.com">sales@bestpractical.com</a>' &>To inquire about support, training, custom development or licensing, please contact [_1].</&><br /></p> % } </div> % if ($Debug >= 2 ) { diff --git a/share/html/Elements/HeaderJavascript b/share/html/Elements/HeaderJavascript index e392ac262d..28788db578 100644 --- a/share/html/Elements/HeaderJavascript +++ b/share/html/Elements/HeaderJavascript @@ -60,14 +60,14 @@ $onload => undef <script type="text/javascript"><!-- jQuery( loadTitleBoxStates ); % if ( $focus ) { - jQuery(function () { focusElementById('<% $focus %>') }); + jQuery(function () { focusElementById(<% $focus |n,j%>) }); % } % if ( $onload ) { jQuery( <% $onload |n %> ); % } % if ( $RichText and RT->Config->Get('MessageBoxRichText', $session{'CurrentUser'})) { - jQuery().ready(function () { ReplaceAllTextareas('<%$m->request_args->{'CKeditorEncoded'} || 0 %>') }); + jQuery().ready(function () { ReplaceAllTextareas(<%$m->request_args->{'CKeditorEncoded'} || 0 |n,j%>) }); % } --></script> <%ARGS> diff --git a/share/html/Elements/MessageBox b/share/html/Elements/MessageBox index 2943cab4e4..61995e057c 100755 --- a/share/html/Elements/MessageBox +++ b/share/html/Elements/MessageBox @@ -45,7 +45,7 @@ %# those contributions and any derivatives thereof. %# %# END BPS TAGGED BLOCK }}} -<textarea autocomplete="off" class="messagebox" <% $cols |n %> rows="<% $Height %>" <% $wrap_type |n %> name="<% $Name %>" id="<% $Name %>">\ +<textarea autocomplete="off" class="messagebox" <% $width_attr %>="<% $Width %>" rows="<% $Height %>" <% $wrap_type |n %> name="<% $Name %>" id="<% $Name %>">\ % $m->comp('/Articles/Elements/IncludeArticle', %ARGS); % $m->callback( %ARGS, SignatureRef => \$signature ); <% $Default || '' %><% $message %><% $signature %></textarea> @@ -68,13 +68,16 @@ if ( $IncludeSignature and my $text = $session{'CurrentUser'}->UserObj->Signatur # wrap="something" seems to really break IE + richtext my $wrap_type = ''; if ( not RT->Config->Get('MessageBoxRichText', $session{'CurrentUser'}) ) { - $wrap_type = qq(wrap="$Wrap"); + $wrap_type = 'wrap="' . $m->interp->apply_escapes($Wrap, 'h') . '"'; } -# If there's no cols specified, we want to set the width to 100% -my $cols = 'style="width: 100%"'; -if ( defined $Width and length $Width ) { - $cols = qq(cols="$Width"); +# If there's no cols specified, we want to set the width to 100% in CSS +my $width_attr; +if ($Width) { + $width_attr = 'cols'; +} else { + $width_attr = 'style'; + $Width = 'width: 100%'; } </%INIT> diff --git a/share/html/Elements/RT__CustomField/ColumnMap b/share/html/Elements/RT__CustomField/ColumnMap index 06e2674ca1..ecb219d9e2 100644 --- a/share/html/Elements/RT__CustomField/ColumnMap +++ b/share/html/Elements/RT__CustomField/ColumnMap @@ -120,8 +120,10 @@ my $COLUMN_MAP = { my $name = 'RemoveCustomField'; my $checked = $m->request_args->{ $name .'All' }? 'checked="checked"': ''; - return \qq{<input type="checkbox" name="${name}All" value="1" $checked - onclick="setCheckbox(this.form, '$name', this.checked)" />}; + return \qq{<input type="checkbox" name="}, $name, \qq{All" value="1" $checked + onclick="setCheckbox(this.form, }, + $m->interp->apply_escapes($name,'j'), + \qq{, this.checked)" />}; }, value => sub { my $id = $_[0]->id; @@ -137,7 +139,7 @@ my $COLUMN_MAP = { elsif ( $arg ) { $checked = 'checked="checked"' if $arg == $id; } - return \qq{<input type="checkbox" name="$name" value="$id" $checked />} + return \qq{<input type="checkbox" name="}, $name, \qq{" value="$id" $checked />} }, }, MoveCF => { diff --git a/share/html/Elements/RT__Dashboard/ColumnMap b/share/html/Elements/RT__Dashboard/ColumnMap index 8bc4383d83..6c366ec78a 100644 --- a/share/html/Elements/RT__Dashboard/ColumnMap +++ b/share/html/Elements/RT__Dashboard/ColumnMap @@ -111,7 +111,7 @@ my $COLUMN_MAP = { } } - return \('<a href="'.$url.'">'.$frequency.'</a>'); + return \'<a href="', $url, \'">', $frequency, \'</a>'; }, }, ShowURL => { diff --git a/share/html/Elements/SelectOwnerAutocomplete b/share/html/Elements/SelectOwnerAutocomplete index cf2010a805..81b38386c3 100644 --- a/share/html/Elements/SelectOwnerAutocomplete +++ b/share/html/Elements/SelectOwnerAutocomplete @@ -78,7 +78,7 @@ my $query = $m->comp('/Elements/QueryString', <script type="text/javascript"> jQuery(function() { var cache = {}; - jQuery("#<% $Name %>").autocomplete({ + jQuery("#"+<% $Name |n,j%>).autocomplete({ minLength: 2, source: function(request, response) { if ( request.term in cache ) { @@ -86,7 +86,7 @@ my $query = $m->comp('/Elements/QueryString', } else { jQuery.ajax({ - url: "<% RT->Config->Get('WebPath')%>/Helpers/Autocomplete/Owners?<% $query|n %>", + url: <% RT->Config->Get('WebPath')|n,j%>+"/Helpers/Autocomplete/Owners?"+<% $query|n,j %>, dataType: "json", data: request, success: function( data ) { diff --git a/share/html/Elements/ShowCustomFields b/share/html/Elements/ShowCustomFields index fcd530e956..6059f4ee76 100644 --- a/share/html/Elements/ShowCustomFields +++ b/share/html/Elements/ShowCustomFields @@ -114,12 +114,12 @@ my $print_value = sub { my $vid = $value->id; $m->out( '<div class="object_cf_value_include" id="object_cf_value_'. $vid .'">' ); $m->out( loc("See also:") ); - $m->out( '<a href="'. $value->IncludeContentForValue .'">' ); - $m->out( $value->IncludeContentForValue ); + $m->out( '<a href="'. $m->interp->apply_escapes($value->IncludeContentForValue, 'h') .'">' ); + $m->out( $m->interp->apply_escapes($value->IncludeContentForValue, 'h') ); $m->out( qq{</a></div>\n} ); - $m->out( qq{<script><!--\njQuery('#object_cf_value_$vid').load('} ); - $m->out( $value->IncludeContentForValue ); - $m->out( qq{');\n--></script>\n} ); + $m->out( qq{<script><!--\njQuery('#object_cf_value_$vid').load(} ); + $m->out( $m->interp->apply_escapes($value->IncludeContentForValue, 'j') ); + $m->out( qq{);\n--></script>\n} ); } }; diff --git a/share/html/Elements/ShowSearch b/share/html/Elements/ShowSearch index 4ae200eb79..4b96bbfda3 100644 --- a/share/html/Elements/ShowSearch +++ b/share/html/Elements/ShowSearch @@ -64,12 +64,12 @@ my $query_link_url = RT->Config->Get('WebPath').'/Search/Results.html'; if ($SavedSearch) { my ( $container_object, $search_id ) = _parse_saved_search($SavedSearch); unless ( $container_object ) { - $m->out(loc("Either you have no rights to view saved search [_1] or identifier is incorrect", $SavedSearch)); + $m->out(loc("Either you have no rights to view saved search [_1] or identifier is incorrect", $m->interp->apply_escapes($SavedSearch, 'h'))); return; } $search = $container_object->Attributes->WithId($search_id); unless ( $search->Id && ref( $SearchArg = $search->Content ) eq 'HASH' ) { - $m->out(loc("Saved Search [_1] not found", $SavedSearch)) unless $IgnoreMissing; + $m->out(loc("Saved Search [_1] not found", $m->interp->apply_escapes($SavedSearch, 'h'))) unless $IgnoreMissing; return; } $SearchArg->{'SavedSearchId'} ||= $SavedSearch; @@ -93,7 +93,7 @@ if ($SavedSearch) { if ($custom->Description eq $Name) { $search = $custom; last } } unless ($search && $search->id) { - $m->out("Predefined search $Name not found"); + $m->out(loc("Predefined search [_1] not found", $m->interp->apply_escapes($Name, 'h'))); return; } } diff --git a/share/html/Elements/ShowUser b/share/html/Elements/ShowUser index 044ec4c84b..365497765a 100644 --- a/share/html/Elements/ShowUser +++ b/share/html/Elements/ShowUser @@ -51,7 +51,7 @@ # $Address is Email::Address object my $comp = '/Elements/ShowUser'. ucfirst lc $style; -unless ( $m->comp_exists( $comp ) ) { +unless ( RT::Interface::Web->ComponentPathIsSafe($comp) and $m->comp_exists( $comp ) ) { $RT::Logger->error( 'Either system config or user #' . $session{'CurrentUser'}->id diff --git a/share/html/Elements/Submit b/share/html/Elements/Submit index cbf3f58e82..b7840d34be 100755 --- a/share/html/Elements/Submit +++ b/share/html/Elements/Submit @@ -52,10 +52,10 @@ id="<%$id%>" > <div class="extra-buttons"> % if ($CheckAll) { - <input type="button" value="<%$CheckAllLabel%>" onclick="setCheckbox(this.form, <% length $CheckboxName ? qq{'$CheckboxName'} : length $CheckboxNameRegex ? $CheckboxNameRegex : q{''} %>, true);return false;" class="button" /> + <input type="button" value="<%$CheckAllLabel%>" onclick="setCheckbox(this.form, <% $match %>, true);return false;" class="button" /> % } % if ($ClearAll) { - <input type="button" value="<%$ClearAllLabel%>" onclick="setCheckbox(this.form, <% length $CheckboxName ? qq{'$CheckboxName'} : length $CheckboxNameRegex ? $CheckboxNameRegex : q{''} %>, false);return false;" class="button" /> + <input type="button" value="<%$ClearAllLabel%>" onclick="setCheckbox(this.form, <% $match %>, false);return false;" class="button" /> % } % if ($Reset) { <input type="reset" value="<%$ResetLabel%>" class="button" /> @@ -115,3 +115,13 @@ $ResetLabel => loc('Reset') $SubmitId => undef $id => undef </%ARGS> +<%init> +my $match; +if (length $CheckboxName) { + $match = $m->interp->apply_escapes($CheckboxName,'j'); +} elsif (length $CheckboxNameRegex) { + $match = $CheckboxNameRegex; +} else { + $match = q{''}; +} +</%init> diff --git a/share/html/Elements/Tabs b/share/html/Elements/Tabs index f65f90975d..ccfbd4c162 100755 --- a/share/html/Elements/Tabs +++ b/share/html/Elements/Tabs @@ -788,7 +788,7 @@ my $build_main_nav = sub { my $tabs = PageMenu(); $tabs->child( search => title => loc("Search"), path => "/Articles/Article/Search.html" ); $tabs->child( create => title => loc("New Article" ), path => "/Articles/Article/PreCreate.html" ); - if ( ( $m->request_args->{'id'} || '' ) =~ /^(\d+)$/ ) { + if ( $request_path =~ m{^/Articles/Article/} and ( $m->request_args->{'id'} || '' ) =~ /^(\d+)$/ ) { my $id = $1; my $obj = RT::Article->new( $session{'CurrentUser'} ); $obj->Load($id); diff --git a/share/html/Helpers/Autocomplete/CustomFieldValues b/share/html/Helpers/Autocomplete/CustomFieldValues index b8b21e4fea..887302f0c6 100644 --- a/share/html/Helpers/Autocomplete/CustomFieldValues +++ b/share/html/Helpers/Autocomplete/CustomFieldValues @@ -52,6 +52,17 @@ # Only autocomplete the last value my $term = (split /\n/, $ARGS{term} || '')[-1]; +my $abort = sub { + $r->content_type('application/json'); + $m->out(JSON::to_json( [] )); + $m->abort; +}; + +unless ( exists $ARGS{ContextType} and exists $ARGS{ContextId} ) { + RT->Logger->debug("No context provided"); + $abort->(); +} + my $CustomField; for my $k ( keys %ARGS ) { next unless $k =~ /^Object-.*?-\d*-CustomField-(\d+)-Values?$/; @@ -59,9 +70,38 @@ for my $k ( keys %ARGS ) { last; } -$m->abort unless $CustomField; +unless ( $CustomField ) { + RT->Logger->debug("No CustomField provided"); + $abort->(); +} + +my $SystemCustomFieldObj = RT::CustomField->new( RT->SystemUser ); +my ($id, $msg) = $SystemCustomFieldObj->LoadById( $CustomField ) ; +unless ( $id ) { + RT->Logger->debug("Invalid CustomField provided: $msg"); + $abort->(); +} + +my $context_object = $SystemCustomFieldObj->LoadContextObject( + $ARGS{ContextType}, $ARGS{ContextId} ); +$abort->() unless $context_object; + my $CustomFieldObj = RT::CustomField->new( $session{'CurrentUser'} ); -$CustomFieldObj->Load( $CustomField ); +if ( $SystemCustomFieldObj->ValidateContextObject($context_object) ) { + # drop our privileges that came from calling LoadContextObject as the System User + $context_object->new($session{'CurrentUser'}); + $context_object->LoadById($ARGS{ContextId}); + $CustomFieldObj->SetContextObject( $context_object ); +} else { + RT->Logger->debug("Invalid Context Object ".$context_object->id." for Custom Field ".$SystemCustomFieldObj->id); + $abort->(); +} + +($id, $msg) = $CustomFieldObj->LoadById( $CustomField ); +unless ( $CustomFieldObj->Name ) { + RT->Logger->debug("Current User cannot see this Custom Field, terminating"); + $abort->(); +} my $values = $CustomFieldObj->Values; $values->Limit( diff --git a/share/html/Helpers/Toggle/ShowRequestor b/share/html/Helpers/Toggle/ShowRequestor index bb90b98876..68e8a0517c 100644 --- a/share/html/Helpers/Toggle/ShowRequestor +++ b/share/html/Helpers/Toggle/ShowRequestor @@ -47,7 +47,9 @@ %# END BPS TAGGED BLOCK }}} <%INIT> my $TicketTemplate = "/Ticket/Elements/ShowRequestorTickets$Status"; -$TicketTemplate = "/Ticket/Elements/ShowRequestorTicketsActive" unless $m->comp_exists($TicketTemplate); +$TicketTemplate = "/Ticket/Elements/ShowRequestorTicketsActive" + unless RT::Interface::Web->ComponentPathIsSafe($TicketTemplate) + and $m->comp_exists($TicketTemplate); my $user_obj = RT::User->new($session{CurrentUser}); my ($val, $msg) = $user_obj->Load($Requestor); unless ($val) { diff --git a/share/html/Install/DatabaseType.html b/share/html/Install/DatabaseType.html index 3312b57ec2..68f8a67ed8 100644 --- a/share/html/Install/DatabaseType.html +++ b/share/html/Install/DatabaseType.html @@ -58,7 +58,7 @@ <&|/l&>SQLite is a database that doesn't need a server or any configuration whatsoever. RT's authors recommend it for testing, demoing and development, but it's not quite right for a high-volume production RT server.</&> </b></p> <p> -<&|/l, '<a href="http://search.cpan.org" target="_new">CPAN</a>' &>If your preferred database isn't listed in the dropdown below, that means RT couldn't find a <i>database driver</i> for it installed locally. You may be able to remedy this by using [_1] to download and install DBD::MySQL, DBD::Oracle or DBD::Pg.</&> +<&|/l_unsafe, '<a href="http://search.cpan.org" target="_new">CPAN</a>' &>If your preferred database isn't listed in the dropdown below, that means RT couldn't find a <i>database driver</i> for it installed locally. You may be able to remedy this by using [_1] to download and install DBD::MySQL, DBD::Oracle or DBD::Pg.</&> </p> </div> diff --git a/share/html/Install/Finish.html b/share/html/Install/Finish.html index ee81e70e4f..24ac0ff71d 100644 --- a/share/html/Install/Finish.html +++ b/share/html/Install/Finish.html @@ -53,7 +53,7 @@ </p> <p> -<&|/l, '<tt>root</tt>' &>You should be taken directly to a login page. You'll be able to log in with username of [_1] and the password you set earlier.</&> +<&|/l_unsafe, '<tt>root</tt>' &>You should be taken directly to a login page. You'll be able to log in with username of [_1] and the password you set earlier.</&> </p> <p> diff --git a/share/html/NoAuth/js/titlebox-state.js b/share/html/NoAuth/js/titlebox-state.js index 51996377ef..a5e5dac20d 100644 --- a/share/html/NoAuth/js/titlebox-state.js +++ b/share/html/NoAuth/js/titlebox-state.js @@ -46,7 +46,7 @@ %# %# END BPS TAGGED BLOCK }}} function createCookie(name,value,days) { - var path = "<%RT->Config->Get('WebPath')%>" ? "<%RT->Config->Get('WebPath')%>" : "/"; + var path = <%RT->Config->Get('WebPath')|n,j%> ? <%RT->Config->Get('WebPath')|n,j%> : "/"; if (days) { var date = new Date(); diff --git a/share/html/NoAuth/js/userautocomplete.js b/share/html/NoAuth/js/userautocomplete.js index db244d16c5..b4f678c76e 100644 --- a/share/html/NoAuth/js/userautocomplete.js +++ b/share/html/NoAuth/js/userautocomplete.js @@ -70,7 +70,7 @@ jQuery(function() { continue; var options = { - source: "<% RT->Config->Get('WebPath')%>/Helpers/Autocomplete/Users" + source: <% RT->Config->Get('WebPath') |n,j%>+"/Helpers/Autocomplete/Users" }; var queryargs = []; diff --git a/share/html/NoAuth/js/util.js b/share/html/NoAuth/js/util.js index 62ee922f9c..5bfce411d2 100644 --- a/share/html/NoAuth/js/util.js +++ b/share/html/NoAuth/js/util.js @@ -294,8 +294,8 @@ function ReplaceAllTextareas(encoded) { textArea.parentNode.appendChild(typeField); - CKEDITOR.replace(textArea.name,{width:'100%',height:'<% RT->Config->Get('MessageBoxRichTextHeight') %>'}); - CKEDITOR.basePath = "<%RT->Config->Get('WebPath')%>/NoAuth/RichText/"; + CKEDITOR.replace(textArea.name,{width:'100%',height:<% RT->Config->Get('MessageBoxRichTextHeight') |n,j%>}); + CKEDITOR.basePath = <%RT->Config->Get('WebPath')|n,j%>+"/NoAuth/RichText/"; jQuery("#" + textArea.name + "___Frame").addClass("richtext-editor"); } diff --git a/share/html/REST/1.0/Forms/transaction/default b/share/html/REST/1.0/Forms/transaction/default index 1ffa2b2a56..2e45f67076 100644 --- a/share/html/REST/1.0/Forms/transaction/default +++ b/share/html/REST/1.0/Forms/transaction/default @@ -49,7 +49,6 @@ %# <%ARGS> $id -$args => undef $format => undef $fields => undef </%ARGS> @@ -57,8 +56,6 @@ $fields => undef my $trans = RT::Transactions->new($session{CurrentUser}); my ($c, $o, $k, $e) = ("", [], {} , ""); -chomp $args; -my @arglist = split('/', $args); my $tid = $id; $trans->Limit(FIELD => 'Id', OPERATOR => '=', VALUE => $tid); diff --git a/share/html/Search/Chart.html b/share/html/Search/Chart.html index 884d1838af..070ce7cf7d 100644 --- a/share/html/Search/Chart.html +++ b/share/html/Search/Chart.html @@ -124,7 +124,7 @@ my %query; <input type="hidden" class="hidden" name="Query" value="<% $ARGS{Query} %>" /> <input type="hidden" class="hidden" name="SavedChartSearchId" value="<% $saved_search->{SearchId} || 'new' %>" /> -<&|/l, $m->scomp('Elements/SelectChartType', Name => 'ChartStyle', Default => $ChartStyle), $m->scomp('Elements/SelectGroupBy', Name => 'PrimaryGroupBy', Query => $ARGS{Query}, Default => $PrimaryGroupBy) +<&|/l_unsafe, $m->scomp('Elements/SelectChartType', Name => 'ChartStyle', Default => $ChartStyle), $m->scomp('Elements/SelectGroupBy', Name => 'PrimaryGroupBy', Query => $ARGS{Query}, Default => $PrimaryGroupBy) &>[_1] chart by [_2]</&><input type="submit" class="button" value="<%loc('Update Chart')%>" /> </form> </&> diff --git a/share/html/Search/Simple.html b/share/html/Search/Simple.html index 07bd2f4dc9..4d7b1e3c51 100644 --- a/share/html/Search/Simple.html +++ b/share/html/Search/Simple.html @@ -60,7 +60,7 @@ % my @strong = qw(<strong> </strong>); -<p><&|/l, @strong &>Search for tickets by entering [_1]id[_2] numbers, subject words [_1]"in quotes"[_2], [_1]queues[_2] by name, Owners by [_1]username[_2], Requestors by [_1]email address[_2], and ticket [_1]statuses[_2].</&></p> +<p><&|/l_unsafe, @strong &>Search for tickets by entering [_1]id[_2] numbers, subject words [_1]"in quotes"[_2], [_1]queues[_2] by name, Owners by [_1]username[_2], Requestors by [_1]email address[_2], and ticket [_1]statuses[_2].</&></p> <p><&|/l&>Any word not recognized by RT is searched for in ticket subjects.</&></p> @@ -74,7 +74,7 @@ % } % } -<p><&|/l, map { "<strong>$_</strong>" } qw(initial active inactive any) &>Entering [_1], [_2], [_3], or [_4] limits results to tickets with one of the respective types of statuses. Any individual status name limits results to just the statuses named.</&> +<p><&|/l_unsafe, map { "<strong>$_</strong>" } qw(initial active inactive any) &>Entering [_1], [_2], [_3], or [_4] limits results to tickets with one of the respective types of statuses. Any individual status name limits results to just the statuses named.</&> % if (RT->Config->Get('OnlySearchActiveTicketsInSimpleSearch', $session{'CurrentUser'})) { % my $status_str = join ', ', map { loc($_) } RT::Queue->ActiveStatusArray; @@ -82,13 +82,13 @@ % } </p> -<p><&|/l, map { "<strong>$_</strong>" } 'queue:"Example Queue"', 'owner:email@example.com' &>Start the search term with the name of a supported field followed by a colon, as in [_1] and [_2], to explicitly specify the search type.</&></p> +<p><&|/l_unsafe, map { "<strong>$_</strong>" } 'queue:"Example Queue"', 'owner:email@example.com' &>Start the search term with the name of a supported field followed by a colon, as in [_1] and [_2], to explicitly specify the search type.</&></p> -<p><&|/l, '<strong>cf.Name:value</strong>' &>CFs may be searched using a similar syntax as above with [_1].</&></p> +<p><&|/l_unsafe, '<strong>cf.Name:value</strong>' &>CFs may be searched using a similar syntax as above with [_1].</&></p> % my $link_start = '<a href="' . RT->Config->Get('WebPath') . '/Search/Build.html">'; % my $link_end = '</a>'; -<p><&|/l, $link_start, $link_end &>For the full power of RT's searches, please visit the [_1]search builder interface[_2].</&></p> +<p><&|/l_unsafe, $link_start, $link_end &>For the full power of RT's searches, please visit the [_1]search builder interface[_2].</&></p> </form> diff --git a/share/html/SelfService/Elements/MyRequests b/share/html/SelfService/Elements/MyRequests index 549d458394..76accfc9be 100755 --- a/share/html/SelfService/Elements/MyRequests +++ b/share/html/SelfService/Elements/MyRequests @@ -45,42 +45,34 @@ %# those contributions and any derivatives thereof. %# %# END BPS TAGGED BLOCK }}} -<&| /Widgets/TitleBox, title => $title &> +<&| /Widgets/TitleBox, title => $title &> <& /Elements/CollectionList, Title => $title, Format => $Format, Query => $Query, Order => @Order, OrderBy => @OrderBy, BaseURL => $BaseURL, - GenericQueryArgs => $GenericQueryArgs, - AllowSorting => $AllowSorting, + AllowSorting => 1, Class => 'RT::Tickets', Rows => $Rows, Page => $Page &> </&> <%INIT> +my $title = loc("My [_1] tickets", $friendly_status); my $id = $session{'CurrentUser'}->id; -my $Query = "( " - . join( ' OR ', map "$_.id = $id", @roles ) - . ")"; +my $Query = "( Watcher.id = $id )"; if ( @status ) { - $Query .= " AND ( " - . join( ' OR ', map "Status = '$_'", @status ) - . " )"; + @status = map {s/(['\\])/\\$1/g; "Status = '$_'"} @status; + $Query .= " AND ( " . join(' OR ', @status ) . " )"; } my $Format = RT->Config->Get('DefaultSelfServiceSearchResultFormat'); - </%INIT> <%ARGS> $friendly_status => loc('open') -$title => loc("My [_1] tickets", $friendly_status) -@roles => ('Watcher') -@status => RT::Queue->ActiveStatusArray() +@status => () $BaseURL => undef $Page => 1 -$GenericQueryArgs => undef -$AllowSorting => 1 @Order => ('ASC') @OrderBy => ('Created') $Rows => 50 diff --git a/share/html/SelfService/index.html b/share/html/SelfService/index.html index a89296b199..29accf5517 100755 --- a/share/html/SelfService/index.html +++ b/share/html/SelfService/index.html @@ -48,6 +48,8 @@ <& /SelfService/Elements/Header, Title => loc('Open tickets') &> <& /SelfService/Elements/MyRequests, %ARGS, + status => [ RT::Queue->ActiveStatusArray() ], + friendly_status => loc('open'), BaseURL => RT->Config->Get('WebPath') ."/SelfService/?", Page => $Page, &> diff --git a/share/html/Ticket/Elements/Bookmark b/share/html/Ticket/Elements/Bookmark index 83931918de..30c9a4356e 100644 --- a/share/html/Ticket/Elements/Bookmark +++ b/share/html/Ticket/Elements/Bookmark @@ -83,7 +83,7 @@ $Toggle => 0 </%ARGS> <span class="toggle-bookmark-<% $id %>"> % my $url = RT->Config->Get('WebPath') ."/Helpers/Toggle/TicketBookmark?id=". $id; -<a align="right" href="<% $url %>" onclick="jQuery('.toggle-bookmark-<% $id |n%>').load('<% $url |n %>'); return false;" > +<a align="right" href="<% $url %>" onclick="jQuery('.toggle-bookmark-'+<% $id |n,j%>).load(<% $url |n,j %>); return false;" > % if ( $bookmarked ) { <img src="<% RT->Config->Get('WebPath') %>/NoAuth/images/star.gif" alt="<% loc('Remove Bookmark') %>" style="border-style: none" /> % } else { diff --git a/share/html/Ticket/Elements/ClickToShowHistory b/share/html/Ticket/Elements/ClickToShowHistory index 4461b9af6e..5a9a477e07 100644 --- a/share/html/Ticket/Elements/ClickToShowHistory +++ b/share/html/Ticket/Elements/ClickToShowHistory @@ -47,7 +47,7 @@ %# END BPS TAGGED BLOCK }}} <div id="deferred_ticket_history"> <& /Widgets/TitleBoxStart, title => 'History' &> - <a href="<% $display %>" onclick="jQuery('#deferred_ticket_history').text('<% loc('Loading...') %>').load('<% $url |n %>'); return false;" ><% loc('Show ticket history') %></a> + <a href="<% $display %>" onclick="jQuery('#deferred_ticket_history').text(<% loc('Loading...') |n,j%>).load(<% $url |n,j %>); return false;" ><% loc('Show ticket history') %></a> <& /Widgets/TitleBoxEnd &> </div> <%ARGS> diff --git a/share/html/Ticket/Elements/FoldStanzaJS b/share/html/Ticket/Elements/FoldStanzaJS index be6b2648c1..4b0b4c4660 100644 --- a/share/html/Ticket/Elements/FoldStanzaJS +++ b/share/html/Ticket/Elements/FoldStanzaJS @@ -47,4 +47,4 @@ %# END BPS TAGGED BLOCK }}} <span class="message-stanza-folder closed" - onclick="fold_message_stanza(this, '<%loc('Show quoted text')%>', '<%loc('Hide quoted text')%>');"><%loc('Show quoted text')%></span><br />\ + onclick="fold_message_stanza(this, <%loc('Show quoted text') |n,j%>, <%loc('Hide quoted text') |n,j%>);"><%loc('Show quoted text')%></span><br />\ diff --git a/share/html/Ticket/Elements/ShowHistory b/share/html/Ticket/Elements/ShowHistory index b5e009c34c..909ea01eec 100755 --- a/share/html/Ticket/Elements/ShowHistory +++ b/share/html/Ticket/Elements/ShowHistory @@ -60,11 +60,12 @@ if ($ShowDisplayModes or $ShowTitle) { if ($ShowDisplayModes) { $titleright = ''; - my $open_all = $m->interp->apply_escapes( loc("Show all quoted text"), 'h' ); - my $close_all = $m->interp->apply_escapes( loc("Hide all quoted text"), 'h' ); + my $open_all = $m->interp->apply_escapes( loc("Show all quoted text"), 'j' ); + my $open_html = $m->interp->apply_escapes( loc("Show all quoted text"), 'h' ); + my $close_all = $m->interp->apply_escapes( loc("Hide all quoted text"), 'j' ); $titleright .= '<a href="#" data-direction="open" ' - . qq{onclick='return toggle_all_folds(this, "$open_all", "$close_all");'} - . ">$open_all</a> — "; + . qq{onclick="return toggle_all_folds(this, $open_all, $close_all);"} + . ">$open_html</a> — "; if ($ShowHeaders) { $titleright .= qq{<a href="$URIFile?id=} . diff --git a/share/html/Ticket/Elements/ShowRequestor b/share/html/Ticket/Elements/ShowRequestor index 3e5f41fb9c..8a8eef62cf 100755 --- a/share/html/Ticket/Elements/ShowRequestor +++ b/share/html/Ticket/Elements/ShowRequestor @@ -175,7 +175,9 @@ unless ( $DefaultTicketsTab eq 'None' ) { } my $TicketTemplate = "ShowRequestorTickets$DefaultTicketsTab"; -$TicketTemplate = "ShowRequestorTicketsActive" unless $m->comp_exists($TicketTemplate); +$TicketTemplate = "ShowRequestorTicketsActive" + unless RT::Interface::Web->ComponentPathIsSafe($TicketTemplate) + and $m->comp_exists($TicketTemplate); </%INIT> <%ARGS> $Ticket=>undef diff --git a/share/html/Ticket/Elements/UpdateCc b/share/html/Ticket/Elements/UpdateCc index 392ee86b1d..d062156c79 100644 --- a/share/html/Ticket/Elements/UpdateCc +++ b/share/html/Ticket/Elements/UpdateCc @@ -61,8 +61,7 @@ class="onetime onetimecc" type="checkbox" % my $clean_addr = $txn_addresses{$addr}->format; -% $clean_addr =~ s/'/\\'/g; - onClick="checkboxToInput('UpdateCc', 'UpdateCc-<%$addr%>','<%$clean_addr%>' );" + onClick="checkboxToInput('UpdateCc', <% "UpdateCc-$addr" |n,j%>, <%$clean_addr|n,j%> );" <% $ARGS{'UpdateCc-'.$addr} ? 'checked="checked"' : ''%> > <& /Elements/ShowUser, Address => $txn_addresses{$addr}&> %} </td></tr> @@ -77,8 +76,7 @@ class="onetime onetimebcc" type="checkbox" % my $clean_addr = $txn_addresses{$addr}->format; -% $clean_addr =~ s/'/\\'/g; - onClick="checkboxToInput('UpdateBcc', 'UpdateBcc-<%$addr%>','<%$clean_addr%>' );" + onClick="checkboxToInput('UpdateBcc', <% "UpdateBcc-$addr" |n,j%>, <%$clean_addr|n,j%> );" <% $ARGS{'UpdateBcc-'.$addr} ? 'checked="checked"' : ''%>> <& /Elements/ShowUser, Address => $txn_addresses{$addr}&> %} diff --git a/share/html/Ticket/Graphs/Elements/EditGraphProperties b/share/html/Ticket/Graphs/Elements/EditGraphProperties index e68aaed55b..c5479e3f01 100644 --- a/share/html/Ticket/Graphs/Elements/EditGraphProperties +++ b/share/html/Ticket/Graphs/Elements/EditGraphProperties @@ -151,7 +151,7 @@ my $class = ''; $class = 'class="hidden"' if $Level != 1 && !@Default; </%INIT> <% loc('Show Tickets Properties on [_1] level', $Level) %> -(<small><a href="#" onclick="hideshow('<% $id %>'); return false;"><% loc('open/close') %></a></small>): +(<small><a href="#" onclick="hideshow(<% $id |n,j%>); return false;"><% loc('open/close') %></a></small>): <table id="<% $id %>" <% $class |n %>> % while ( my ($group, $list) = (splice @Available, 0, 2) ) { <tr><td><% loc($group) %>:</td><td> diff --git a/share/html/Ticket/Graphs/Elements/ShowGraph b/share/html/Ticket/Graphs/Elements/ShowGraph index 967a5e4aa4..2163b81a8a 100644 --- a/share/html/Ticket/Graphs/Elements/ShowGraph +++ b/share/html/Ticket/Graphs/Elements/ShowGraph @@ -66,6 +66,7 @@ $ARGS{'id'} = $id = $ticket->id; require RT::Graph::Tickets; my $graph = RT::Graph::Tickets->TicketLinks( %ARGS, + Graph => undef, Ticket => $ticket, ); </%INIT> diff --git a/share/html/Ticket/Graphs/dhandler b/share/html/Ticket/Graphs/dhandler index ba41445d07..89b1f37e16 100644 --- a/share/html/Ticket/Graphs/dhandler +++ b/share/html/Ticket/Graphs/dhandler @@ -65,6 +65,7 @@ unless ( $ticket->id ) { require RT::Graph::Tickets; my $graph = RT::Graph::Tickets->TicketLinks( %ARGS, + Graph => undef, Ticket => $ticket, ); diff --git a/share/html/Widgets/ComboBox b/share/html/Widgets/ComboBox index 238087f0a2..69ac0793bc 100644 --- a/share/html/Widgets/ComboBox +++ b/share/html/Widgets/ComboBox @@ -56,7 +56,7 @@ my $z_index = 9999; <div id="<% $Name %>_Container" class="combobox <%$Class%>" style="z-index: <%$z_index--%>"> <input name="<% $Name %>" id="<% $Name %>" class="combo-text" value="<% $Default || '' %>" type="text" <% $Size ? "size='$Size'" : '' |n %> autocomplete="off" /> -<br style="display: none" /><span id="<% $Name %>_Button" class="combo-button">▼</span><select name="List-<% $Name %>" id="<% $Name %>_List" class="combo-list" onchange="ComboBox_SimpleAttach(this, this.form['<% $Name %>']); " size="<% $Rows %>"> +<br style="display: none" /><span id="<% $Name %>_Button" class="combo-button">▼</span><select name="List-<% $Name %>" id="<% $Name %>_List" class="combo-list" onchange="ComboBox_SimpleAttach(this, this.form[<% $Name |n,j%>]); " size="<% $Rows %>"> <option style="display: none" value="">-</option> % foreach my $value (@Values) { <option value="<%$value%>"><% $value%></option> @@ -64,7 +64,7 @@ my $z_index = 9999; </select> </div> <script language="javascript"><!-- -ComboBox_InitWith('<% $Name %>'); +ComboBox_InitWith(<% $Name |n,j %>); //--></script> </nobr> <%ARGS> diff --git a/share/html/Widgets/TitleBoxStart b/share/html/Widgets/TitleBoxStart index a031477852..cbcc5c3d59 100755 --- a/share/html/Widgets/TitleBoxStart +++ b/share/html/Widgets/TitleBoxStart @@ -48,7 +48,7 @@ <div class="titlebox<% $class ? " $class " : '' %><% $rolledup ? " rolled-up" : ""%>" id="<% $id %>"> <div class="titlebox-title<% $title_class ? " $title_class" : ''%>"> % if ($hideable) { - <span class="widget"><a href="#" onclick="return rollup('<%$tid%>');" title="Toggle visibility"></a></span> + <span class="widget"><a href="#" onclick="return rollup(<%$tid|n,j%>);" title="Toggle visibility"></a></span> % } <span class="left"><% $title_href ? qq[<a href="$title_href">] : '' | n diff --git a/share/html/index.html b/share/html/index.html index d85fbaa3d1..523697b1dd 100755 --- a/share/html/index.html +++ b/share/html/index.html @@ -135,7 +135,7 @@ if ( $ARGS{'QuickCreate'} ) { if ( $ARGS{'q'} ) { - RT::Interface::Web::Redirect(RT->Config->Get('WebURL')."Search/Simple.html?q=".$m->interp->apply_escapes($ARGS{q})); + RT::Interface::Web::Redirect(RT->Config->Get('WebURL')."Search/Simple.html?q=".$m->interp->apply_escapes($ARGS{q}, 'u')); } </%init> diff --git a/share/html/l b/share/html/l index 6396bc640f..9f1b343654 100755 --- a/share/html/l +++ b/share/html/l @@ -47,6 +47,6 @@ %# END BPS TAGGED BLOCK }}} <%init> my $hand = ($session{'CurrentUser'} ||= RT::CurrentUser->new)->LanguageHandle; - $m->print($hand->maketext($m->content,@_)); + $m->print($hand->maketext($m->content,map { $m->interp->apply_escapes($_, 'h') } @_)); return(1); </%init> diff --git a/share/html/l_unsafe b/share/html/l_unsafe new file mode 100755 index 0000000000..6396bc640f --- /dev/null +++ b/share/html/l_unsafe @@ -0,0 +1,52 @@ +%# BEGIN BPS TAGGED BLOCK {{{ +%# +%# COPYRIGHT: +%# +%# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +%# <sales@bestpractical.com> +%# +%# (Except where explicitly superseded by other copyright notices) +%# +%# +%# LICENSE: +%# +%# This work is made available to you under the terms of Version 2 of +%# the GNU General Public License. A copy of that license should have +%# been provided with this software, but in any event can be snarfed +%# from www.gnu.org. +%# +%# This work is distributed in the hope that it will be useful, but +%# WITHOUT ANY WARRANTY; without even the implied warranty of +%# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +%# General Public License for more details. +%# +%# You should have received a copy of the GNU General Public License +%# along with this program; if not, write to the Free Software +%# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +%# 02110-1301 or visit their web page on the internet at +%# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +%# +%# +%# CONTRIBUTION SUBMISSION POLICY: +%# +%# (The following paragraph is not intended to limit the rights granted +%# to you to modify and distribute this software under the terms of +%# the GNU General Public License and is only of importance to you if +%# you choose to contribute your changes and enhancements to the +%# community by submitting them to Best Practical Solutions, LLC.) +%# +%# By intentionally submitting any modifications, corrections or +%# derivatives to this work, or any other work intended for use with +%# Request Tracker, to Best Practical Solutions, LLC, you confirm that +%# you are the copyright holder for those contributions and you grant +%# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +%# royalty-free, perpetual, license to use, copy, create derivative +%# works based on those contributions, and sublicense and distribute +%# those contributions and any derivatives thereof. +%# +%# END BPS TAGGED BLOCK }}} +<%init> + my $hand = ($session{'CurrentUser'} ||= RT::CurrentUser->new)->LanguageHandle; + $m->print($hand->maketext($m->content,@_)); + return(1); +</%init> diff --git a/share/html/m/_elements/footer b/share/html/m/_elements/footer index aad0020824..0d160c2ee1 100644 --- a/share/html/m/_elements/footer +++ b/share/html/m/_elements/footer @@ -48,7 +48,7 @@ <& /Elements/Logo, ShowName => 1, OnlyCustom => 1 &> <div id="bpscredits"> <div id="copyright"> -<&|/l, '', '', '2012', '<a href="http://www.bestpractical.com?rt='.$RT::VERSION.'">Best Practical Solutions, LLC</a>', &>[_1] RT [_2] Copyright 1996-[_3] [_4].</&> +<&|/l_unsafe, '', '', '2012', '<a href="http://www.bestpractical.com?rt='.$RT::VERSION.'">Best Practical Solutions, LLC</a>', &>[_1] RT [_2] Copyright 1996-[_3] [_4].</&> </div> </div> </body> diff --git a/share/html/m/ticket/create b/share/html/m/ticket/create index 6232e87a98..b42787d78b 100644 --- a/share/html/m/ticket/create +++ b/share/html/m/ticket/create @@ -53,6 +53,7 @@ $CloneTicket => undef $m->callback( CallbackName => "Init", ARGSRef => \%ARGS ); my $Queue = $ARGS{Queue}; +my $escape = sub { $m->interp->apply_escapes(shift, 'h') }; my $showrows = sub { my @pairs = @_; @@ -252,7 +253,7 @@ if ((!exists $ARGS{'AddMoreAttach'}) and (defined($ARGS{'id'}) and $ARGS{'id'} e <%perl> $showrows->( - loc("Subject") => '<input type="text" name="Subject" size="30" maxsize="200" value="'.($ARGS{Subject} || '').'" />'); + loc("Subject") => '<input type="text" name="Subject" size="30" maxsize="200" value="'.$escape->($ARGS{Subject} || '').'" />'); </%perl> <span class="content-label label"><%loc("Describe the issue below")%></span> <& /Elements/MessageBox, exists $ARGS{Content} ? (Default => $ARGS{Content}, IncludeSignature => 0 ) : ( QuoteTransaction => $QuoteTransaction ), Height => 5 &> @@ -413,12 +414,12 @@ $showrows->( <%perl> $showrows->( - loc("Depends on") => '<input type="text" size="10" name="new-DependsOn" value="' . ($ARGS{'new-DependsOn'} || '' ). '" />', - loc("Depended on by") => '<input type="text" size="10" name="DependsOn-new" value="' . ($ARGS{'DependsOn-new'} || '' ) . '" />', - loc("Parents") => '<input type="text" size="10" name="new-MemberOf" value="' . ($ARGS{'new-MemberOf'} || '') . '" />', - loc("Children") => '<input type="text" size="10" name="MemberOf-new" value="' . ($ARGS{'MemberOf-new'} || '') . '" />', - loc("Refers to") => '<input type="text" size="10" name="new-RefersTo" value="' . ($ARGS{'new-RefersTo'} || '') . '" />', - loc("Referred to by") => '<input type="text" size="10" name="RefersTo-new" value="' . ($ARGS{'RefersTo-new'} || ''). '" />' + loc("Depends on") => '<input type="text" size="10" name="new-DependsOn" value="' . $escape->($ARGS{'new-DependsOn'} || '' ). '" />', + loc("Depended on by") => '<input type="text" size="10" name="DependsOn-new" value="' . $escape->($ARGS{'DependsOn-new'} || '' ) . '" />', + loc("Parents") => '<input type="text" size="10" name="new-MemberOf" value="' . $escape->($ARGS{'new-MemberOf'} || '') . '" />', + loc("Children") => '<input type="text" size="10" name="MemberOf-new" value="' . $escape->($ARGS{'MemberOf-new'} || '') . '" />', + loc("Refers to") => '<input type="text" size="10" name="new-RefersTo" value="' . $escape->($ARGS{'new-RefersTo'} || '') . '" />', + loc("Referred to by") => '<input type="text" size="10" name="RefersTo-new" value="' . $escape->($ARGS{'RefersTo-new'} || ''). '" />' ); </%perl> diff --git a/share/html/m/ticket/show b/share/html/m/ticket/show index 2b45e90ef2..f6ffe88baa 100644 --- a/share/html/m/ticket/show +++ b/share/html/m/ticket/show @@ -186,18 +186,18 @@ my $print_value = sub { } $m->out('</a>') if defined $linked && length $linked; - # This section automatically populates a<div with the "IncludeContentForValue" for this custom + # This section automatically populates a div with the "IncludeContentForValue" for this custom # field if it's been defined if ( $cf->IncludeContentForValue ) { my $vid = $value->id; $m->out( '<div class="object_cf_value_include" id="object_cf_value_'. $vid .'">' ); $m->print( loc("See also:") ); - $m->out( '<a href="'. $value->IncludeContentForValue .'">' ); - $m->print( $value->IncludeContentForValue ); + $m->out( '<a href="'. $m->interp->apply_escapes($value->IncludeContentForValue, 'h') .'">' ); + $m->out( $m->interp->apply_escapes($value->IncludeContentForValue, 'h') ); $m->out( qq{</a></div>\n} ); - $m->out( qq{<script><!--\nahah('} ); - $m->print( $value->IncludeContentForValue ); - $m->out( qq{', 'object_cf_value_$vid');\n--></script>\n} ); + $m->out( qq{<script><!--\njQuery('#object_cf_value_$vid').load(} ); + $m->out( $m->interp->apply_escapes($value->IncludeContentForValue, 'j') ); + $m->out( qq{);\n--></script>\n} ); } }; diff --git a/share/html/m/tickets/search b/share/html/m/tickets/search index be07381c56..592f994ede 100644 --- a/share/html/m/tickets/search +++ b/share/html/m/tickets/search @@ -78,7 +78,7 @@ my $search; if ( $custom->Description eq $name ) { $search = $custom; last } } unless ( $search && $search->id ) { - $m->out("Predefined search $name not found"); + $m->out(loc("Predefined search [_1] not found", $m->interp->apply_escapes($name, 'h'))); return; } } diff --git a/t/api/date.t b/t/api/date.t index 9756e51c49..6fcaa494b3 100644 --- a/t/api/date.t +++ b/t/api/date.t @@ -4,7 +4,7 @@ use Test::MockTime qw(set_fixed_time restore_time); use DateTime; use warnings; use strict; -use RT::Test tests => 172; +use RT::Test tests => 173; use RT::User; use Test::Warn; @@ -85,9 +85,11 @@ my $current_user; my $date = RT::Date->new(RT->SystemUser); is($date->Unix, 0, "new date returns 0 in Unix format"); is($date->Get, '1970-01-01 00:00:00', "default is ISO format"); - is($date->Get(Format =>'SomeBadFormat'), - '1970-01-01 00:00:00', - "don't know format, return ISO format"); + warning_like { + is($date->Get(Format =>'SomeBadFormat'), + '1970-01-01 00:00:00', + "don't know format, return ISO format"); + } qr/Invalid date formatter/; is($date->Get(Format =>'W3CDTF'), '1970-01-01T00:00:00Z', "W3CDTF format with defaults"); diff --git a/t/web/case-sensitivity.t b/t/web/case-sensitivity.t index 276b7615a9..f984bf3e48 100644 --- a/t/web/case-sensitivity.t +++ b/t/web/case-sensitivity.t @@ -75,7 +75,7 @@ my $cf; # test custom field values auto completer { - $m->get_ok('/Helpers/Autocomplete/CustomFieldValues?term=eNo&Object---CustomField-'. $cf->id .'-Value'); + $m->get_ok('/Helpers/Autocomplete/CustomFieldValues?term=eNo&Object---CustomField-'. $cf->id .'-Value&ContextId=1&ContextType=RT::Queue'); require JSON; is_deeply( JSON::from_json( $m->content ), diff --git a/t/web/csrf-rest.t b/t/web/csrf-rest.t new file mode 100644 index 0000000000..5bb9081655 --- /dev/null +++ b/t/web/csrf-rest.t @@ -0,0 +1,77 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use RT::Test tests => undef; + +my ($baseurl, $m) = RT::Test->started_ok; + +# Get a non-REST session +diag "Standard web session"; +ok $m->login, 'logged in'; +$m->content_contains("RT at a glance", "Get full UI content"); + +# Requesting a REST page should be fine, as we have a Referer +$m->post("$baseurl/REST/1.0/ticket/new", [ + format => 'l', +]); +$m->content_like(qr{^id: ticket/new}m, "REST request with referrer"); + +# Removing the Referer header gets us an interstitial +$m->add_header(Referer => undef); +$m->post("$baseurl/REST/1.0/ticket/new", [ + format => 'l', + foo => 'bar', +]); +$m->content_contains("Possible cross-site request forgery", + "REST request without referrer is blocked"); + +# But passing username and password lets us though +$m->post("$baseurl/REST/1.0/ticket/new", [ + user => 'root', + pass => 'password', + format => 'l', +]); +$m->content_like(qr{^id: ticket/new}m, "REST request without referrer, but username/password supplied, is OK"); + +# And we can still access non-REST urls +$m->get("$baseurl"); +$m->content_contains("RT at a glance", "Full UI is still available"); + + +# Now go get a REST session +diag "REST session"; +$m = RT::Test::Web->new; +$m->post("$baseurl/REST/1.0/ticket/new", [ + user => 'root', + pass => 'password', + format => 'l', +]); +$m->content_like(qr{^id: ticket/new}m, "REST request to log in"); + +# Requesting that page again, with a username/password but no referrer, +# is fine +$m->add_header(Referer => undef); +$m->post("$baseurl/REST/1.0/ticket/new", [ + user => 'root', + pass => 'password', + format => 'l', +]); +$m->content_like(qr{^id: ticket/new}m, "REST request with no referrer, but username/pass"); + +# And it's still fine without both referer and username and password, +# because REST is special-cased +$m->post("$baseurl/REST/1.0/ticket/new", [ + format => 'l', +]); +$m->content_like(qr{^id: ticket/new}m, "REST request with no referrer or username/pass is special-cased for REST sessions"); + +# But the REST page can't request normal pages +$m->get("$baseurl"); +$m->content_lacks("RT at a glance", "Full UI is denied for REST sessions"); +$m->content_contains("This login session belongs to a REST client", "Tells you why"); +$m->warning_like(qr/This login session belongs to a REST client/, "Logs a warning"); + +undef $m; +done_testing; + diff --git a/t/web/csrf.t b/t/web/csrf.t new file mode 100644 index 0000000000..d99b4ce22f --- /dev/null +++ b/t/web/csrf.t @@ -0,0 +1,181 @@ +#!/usr/bin/perl +use strict; +use warnings; + +use RT::Test tests => undef; + +my $ticket = RT::Ticket->new(RT::CurrentUser->new('root')); +my ($ok, $msg) = $ticket->Create(Queue => 1, Owner => 'nobody', Subject => 'bad music'); +ok($ok); +my $other = RT::Test->load_or_create_queue(Name => "Other queue", Disabled => 0); +my $other_queue_id = $other->id; + +my ($baseurl, $m) = RT::Test->started_ok; + +my $test_page = "/Ticket/Create.html?Queue=1"; +my $test_path = "/Ticket/Create.html"; + +ok $m->login, 'logged in'; + +# valid referer +$m->add_header(Referer => $baseurl); +$m->get_ok($test_page); +$m->content_lacks("Possible cross-site request forgery"); +$m->title_is('Create a new ticket'); + +# off-site referer BUT provides auth +$m->add_header(Referer => 'http://example.net'); +$m->get_ok("$test_page&user=root&pass=password"); +$m->content_lacks("Possible cross-site request forgery"); +$m->title_is('Create a new ticket'); + +# explicitly no referer BUT provides auth +$m->add_header(Referer => undef); +$m->get_ok("$test_page&user=root&pass=password"); +$m->content_lacks("Possible cross-site request forgery"); +$m->title_is('Create a new ticket'); + +# now send a referer from an attacker +$m->add_header(Referer => 'http://example.net'); +$m->get_ok($test_page); +$m->content_contains("Possible cross-site request forgery"); +$m->content_contains("If you really intended to visit <tt>/Ticket/Create.html</tt>"); +$m->content_contains("the Referrer header supplied by your browser (example.net:80) is not allowed"); +$m->title_is('Possible cross-site request forgery'); + +# reinstate mech's usual header policy +$m->delete_header('Referer'); + +# clicking the resume request button gets us to the test page +$m->follow_link(text_regex => qr{resume your request}); +$m->content_lacks("Possible cross-site request forgery"); +like($m->response->request->uri, qr{^http://[^/]+\Q$test_path\E\?CSRF_Token=\w+$}); +$m->title_is('Create a new ticket'); + +# try a whitelisted argument from an attacker +$m->add_header(Referer => 'http://example.net'); +$m->get_ok("/Ticket/Display.html?id=1"); +$m->content_lacks("Possible cross-site request forgery"); +$m->title_is('#1: bad music'); + +# now a non-whitelisted argument +$m->get_ok("/Ticket/Display.html?id=1&Action=Take"); +$m->content_contains("Possible cross-site request forgery"); +$m->content_contains("If you really intended to visit <tt>/Ticket/Display.html</tt>"); +$m->content_contains("the Referrer header supplied by your browser (example.net:80) is not allowed"); +$m->title_is('Possible cross-site request forgery'); + +$m->delete_header('Referer'); +$m->follow_link(text_regex => qr{resume your request}); +$m->content_lacks("Possible cross-site request forgery"); +like($m->response->request->uri, qr{^http://[^/]+\Q/Ticket/Display.html}); +$m->title_is('#1: bad music'); +$m->content_contains('Owner changed from Nobody to root'); + +# force mech to never set referer +$m->add_header(Referer => undef); +$m->get_ok($test_page); +$m->content_contains("Possible cross-site request forgery"); +$m->content_contains("If you really intended to visit <tt>/Ticket/Create.html</tt>"); +$m->content_contains("your browser did not supply a Referrer header"); +$m->title_is('Possible cross-site request forgery'); + +$m->follow_link(text_regex => qr{resume your request}); +$m->content_lacks("Possible cross-site request forgery"); +is($m->response->redirects, 0, "no redirection"); +like($m->response->request->uri, qr{^http://[^/]+\Q$test_path\E\?CSRF_Token=\w+$}); +$m->title_is('Create a new ticket'); + +# try sending the wrong csrf token, then the right one +$m->add_header(Referer => undef); +$m->get_ok($test_page); +$m->content_contains("Possible cross-site request forgery"); +$m->content_contains("If you really intended to visit <tt>/Ticket/Create.html</tt>"); +$m->content_contains("your browser did not supply a Referrer header"); +$m->title_is('Possible cross-site request forgery'); + +# Sending a wrong CSRF is just a normal request. We'll make a request +# with just an invalid token, which means no Queue=, which means +# Create.html errors out. +my $link = $m->find_link(text_regex => qr{resume your request}); +(my $broken_url = $link->url) =~ s/(CSRF_Token)=\w+/$1=crud/; +$m->get_ok($broken_url); +$m->content_contains("Queue could not be loaded"); +$m->title_is('RT Error'); +$m->warning_like(qr/Queue could not be loaded/); + +# The token doesn't work for other pages, or other arguments to the same page. +$m->add_header(Referer => undef); +$m->get_ok($test_page); +$m->content_contains("Possible cross-site request forgery"); +my ($token) = $m->content =~ m{CSRF_Token=(\w+)}; + +$m->add_header(Referer => undef); +$m->get_ok("/Admin/Queues/Modify.html?id=new&Name=test&CSRF_Token=$token"); +$m->content_contains("Possible cross-site request forgery"); +$m->content_contains("If you really intended to visit <tt>/Admin/Queues/Modify.html</tt>"); +$m->content_contains("your browser did not supply a Referrer header"); +$m->title_is('Possible cross-site request forgery'); + +$m->follow_link(text_regex => qr{resume your request}); +$m->content_lacks("Possible cross-site request forgery"); +$m->title_is('Configuration for queue test'); + +# Try the same page, but different query parameters, which are blatted by the token +$m->get_ok("/Ticket/Create.html?Queue=$other_queue_id&CSRF_Token=$token"); +$m->content_lacks("Possible cross-site request forgery"); +$m->title_is('Create a new ticket'); +$m->text_unlike(qr/Queue:\s*Other queue/); +$m->text_like(qr/Queue:\s*General/); + +# Ensure that file uploads work across the interstitial +$m->delete_header('Referer'); +$m->get_ok($test_page); +$m->content_contains("Create a new ticket", 'ticket create page'); +$m->form_name('TicketCreate'); +$m->field('Subject', 'Attachments test'); + +my $logofile = "$RT::MasonComponentRoot/NoAuth/images/bpslogo.png"; +open LOGO, "<", $logofile or die "Can't open logo file: $!"; +binmode LOGO; +my $logo_contents = do {local $/; <LOGO>}; +close LOGO; +$m->field('Attach', $logofile); + +# Lose the referer before the POST +$m->add_header(Referer => undef); +$m->submit; +$m->content_contains("Possible cross-site request forgery"); +$m->content_contains("If you really intended to visit <tt>/Ticket/Create.html</tt>"); +$m->follow_link(text_regex => qr{resume your request}); +$m->content_contains('Download bpslogo.png', 'page has file name'); +$m->follow_link_ok({text => "Download bpslogo.png"}); +is($m->content, $logo_contents, "Binary content matches"); + + +# now try self-service with CSRF +my $user = RT::User->new(RT->SystemUser); +$user->Create(Name => "SelfService", Password => "chops", Privileged => 0); + +$m = RT::Test::Web->new; +$m->get_ok("$baseurl/index.html?user=SelfService&pass=chops"); +$m->title_is("Open tickets", "got self-service interface"); +$m->content_contains("My open tickets", "got self-service interface"); + +# post without referer +$m->add_header(Referer => undef); +$m->get_ok("/SelfService/Create.html?Queue=1"); +$m->content_contains("Possible cross-site request forgery"); +$m->content_contains("If you really intended to visit <tt>/SelfService/Create.html</tt>"); +$m->content_contains("your browser did not supply a Referrer header"); +$m->title_is('Possible cross-site request forgery'); + +$m->follow_link(text_regex => qr{resume your request}); +$m->content_lacks("Possible cross-site request forgery"); +is($m->response->redirects, 0, "no redirection"); +like($m->response->request->uri, qr{^http://[^/]+\Q/SelfService/Create.html\E\?CSRF_Token=\w+$}); +$m->title_is('Create a ticket'); +$m->content_contains('Describe the issue below:'); + +undef $m; +done_testing; diff --git a/t/web/owner_disabled_group_19221.t b/t/web/owner_disabled_group_19221.t new file mode 100644 index 0000000000..2664c5bc27 --- /dev/null +++ b/t/web/owner_disabled_group_19221.t @@ -0,0 +1,190 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +use RT::Test tests => undef; + +my $queue = RT::Test->load_or_create_queue( Name => 'Test' ); +ok $queue && $queue->id, 'loaded or created queue'; + +my $user = RT::Test->load_or_create_user( + Name => 'ausername', + Privileged => 1, +); +ok $user && $user->id, 'loaded or created user'; + +my $group = RT::Group->new(RT->SystemUser); +my ($ok, $msg) = $group->CreateUserDefinedGroup(Name => 'Disabled Group'); +ok($ok, $msg); + +($ok, $msg) = $group->AddMember( $user->PrincipalId ); +ok($ok, $msg); + +ok( RT::Test->set_rights({ + Principal => $group, + Object => $queue, + Right => [qw(OwnTicket)] +}), 'set rights'); + +RT->Config->Set( AutocompleteOwners => 0 ); +my ($base, $m) = RT::Test->started_ok; +ok $m->login, 'logged in'; + +diag "user from group shows up in create form"; +{ + $m->get_ok('/', 'open home page'); + $m->form_name('CreateTicketInQueue'); + $m->select( 'Queue', $queue->id ); + $m->submit; + + $m->content_contains('Create a new ticket', 'opened create ticket page'); + my $form = $m->form_name('TicketCreate'); + my $input = $form->find_input('Owner'); + is $input->value, RT->Nobody->Id, 'correct owner selected'; + ok((scalar grep { $_ == $user->Id } $input->possible_values), 'user from group is in dropdown'); +} + +diag "user from disabled group DOESN'T shows up in create form"; +{ + ($ok, $msg) = $group->SetDisabled(1); + ok($ok, $msg); + + $m->get_ok('/', 'open home page'); + $m->form_name('CreateTicketInQueue'); + $m->select( 'Queue', $queue->id ); + $m->submit; + + $m->content_contains('Create a new ticket', 'opened create ticket page'); + my $form = $m->form_name('TicketCreate'); + my $input = $form->find_input('Owner'); + is $input->value, RT->Nobody->Id, 'correct owner selected'; + ok((not scalar grep { $_ == $user->Id } $input->possible_values), 'user from disabled group is NOT in dropdown'); + ($ok, $msg) = $group->SetDisabled(0); + ok($ok, $msg); +} + + + +diag "Put us in a nested group"; +my $super = RT::Group->new(RT->SystemUser); +($ok, $msg) = $super->CreateUserDefinedGroup(Name => 'Supergroup'); +ok($ok, $msg); + +($ok, $msg) = $super->AddMember( $group->PrincipalId ); +ok($ok, $msg); + +ok( RT::Test->set_rights({ + Principal => $super, + Object => $queue, + Right => [qw(OwnTicket)] +}), 'set rights'); + + +diag "Disable the middle group"; +{ + ($ok, $msg) = $group->SetDisabled(1); + ok($ok, "Disabled group: $msg"); + + $m->get_ok('/', 'open home page'); + $m->form_name('CreateTicketInQueue'); + $m->select( 'Queue', $queue->id ); + $m->submit; + + $m->content_contains('Create a new ticket', 'opened create ticket page'); + my $form = $m->form_name('TicketCreate'); + my $input = $form->find_input('Owner'); + is $input->value, RT->Nobody->Id, 'correct owner selected'; + ok((not scalar grep { $_ == $user->Id } $input->possible_values), 'user from disabled group is NOT in dropdown'); + ($ok, $msg) = $group->SetDisabled(0); + ok($ok, "Re-enabled group: $msg"); +} + +diag "Disable the top group"; +{ + ($ok, $msg) = $super->SetDisabled(1); + ok($ok, "Disabled supergroup: $msg"); + + $m->get_ok('/', 'open home page'); + $m->form_name('CreateTicketInQueue'); + $m->select( 'Queue', $queue->id ); + $m->submit; + + $m->content_contains('Create a new ticket', 'opened create ticket page'); + my $form = $m->form_name('TicketCreate'); + my $input = $form->find_input('Owner'); + is $input->value, RT->Nobody->Id, 'correct owner selected'; + ok((not scalar grep { $_ == $user->Id } $input->possible_values), 'user from disabled group is NOT in dropdown'); + ($ok, $msg) = $super->SetDisabled(0); + ok($ok, "Re-enabled supergroup: $msg"); +} + + +diag "Check WithMember and WithoutMember recursively"; +{ + my $with = RT::Groups->new( RT->SystemUser ); + $with->WithMember( PrincipalId => $user->PrincipalObj->Id, Recursively => 1 ); + $with->Limit( FIELD => 'domain', OPERATOR => '=', VALUE => 'UserDefined' ); + is_deeply( + [map {$_->Name} @{$with->ItemsArrayRef}], + ['Disabled Group','Supergroup'], + "Get expected recursive memberships", + ); + + my $without = RT::Groups->new( RT->SystemUser ); + $without->WithoutMember( PrincipalId => $user->PrincipalObj->Id, Recursively => 1 ); + $without->Limit( FIELD => 'domain', OPERATOR => '=', VALUE => 'UserDefined' ); + is_deeply( + [map {$_->Name} @{$without->ItemsArrayRef}], + [], + "And not a member of no groups", + ); + + ($ok, $msg) = $super->SetDisabled(1); + ok($ok, "Disabled supergroup: $msg"); + $with->RedoSearch; + $without->RedoSearch; + is_deeply( + [map {$_->Name} @{$with->ItemsArrayRef}], + ['Disabled Group'], + "Recursive check only contains subgroup", + ); + is_deeply( + [map {$_->Name} @{$without->ItemsArrayRef}], + [], + "Doesn't find the currently disabled group", + ); + ($ok, $msg) = $super->SetDisabled(0); + ok($ok, "Re-enabled supergroup: $msg"); + + ($ok, $msg) = $group->SetDisabled(1); + ok($ok, "Disabled intermediate group: $msg"); + $with->RedoSearch; + $without->RedoSearch; + is_deeply( + [map {$_->Name} @{$with->ItemsArrayRef}], + [], + "Recursive check finds no groups", + ); + is_deeply( + [map {$_->Name} @{$without->ItemsArrayRef}], + ['Supergroup'], + "Now not a member of the supergroup", + ); + ($ok, $msg) = $group->SetDisabled(0); + ok($ok, "Re-enabled intermediate group: $msg"); +} + +diag "Check MemberOfGroup"; +{ + ($ok, $msg) = $group->SetDisabled(1); + ok($ok, "Disabled intermediate group: $msg"); + my $users = RT::Users->new(RT->SystemUser); + $users->MemberOfGroup($super->PrincipalObj->id); + is($users->Count, 0, "Supergroup claims no members"); + ($ok, $msg) = $group->SetDisabled(0); + ok($ok, "Re-enabled intermediate group: $msg"); +} + + +undef $m; +done_testing; diff --git a/t/web/redirect-after-login.t b/t/web/redirect-after-login.t index d429d30d19..835b24c372 100644 --- a/t/web/redirect-after-login.t +++ b/t/web/redirect-after-login.t @@ -196,16 +196,17 @@ for my $path (qw(Prefs/Other.html /Prefs/Other.html)) { # test REST login response { + $agent = RT::Test::Web->new; my $requested = $url."REST/1.0/?user=root;pass=password"; $agent->get($requested); is($agent->status, 200, "Loaded a page"); is($agent->uri, $requested, "didn't redirect to /NoAuth/Login.html for REST"); - $agent->get_ok($url); - $agent->logout(); + $agent->get_ok($url."REST/1.0"); } # test REST login response for wrong pass { + $agent = RT::Test::Web->new; my $requested = $url."REST/1.0/?user=root;pass=passwrong"; $agent->get_ok($requested); is($agent->status, 200, "Loaded a page"); @@ -217,6 +218,7 @@ for my $path (qw(Prefs/Other.html /Prefs/Other.html)) { # test REST login response for no creds { + $agent = RT::Test::Web->new; my $requested = $url."REST/1.0/"; $agent->get_ok($requested); is($agent->status, 200, "Loaded a page"); |