Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/bestpractical/rt.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlex Vandiver <alexmv@bestpractical.com>2012-04-26 04:06:49 +0400
committerAlex Vandiver <alexmv@bestpractical.com>2012-04-26 04:06:49 +0400
commitfa9c4b4b218ea231c048312a3ca0be76b3231a1e (patch)
treeaa9a898098f7635264df331e8d2eb66159584d0a
parent080eb2a7c4b4c790a315da18411d5f2d2d3818ba (diff)
parent21da57aba3248b21240954274bcf5d9a47c92b49 (diff)
Merge branch 'security/4.0-trunk' into 4.0-trunkrt-4.0.6rc1
Conflicts: lib/RT/Interface/Web.pm
-rw-r--r--.gitignore1
-rwxr-xr-xbin/rt-mailgate.in1
-rw-r--r--docs/security.pod15
-rwxr-xr-xetc/RT_Config.pm.in25
-rw-r--r--etc/upgrade/4.0.6/content17
-rwxr-xr-xetc/upgrade/vulnerable-passwords.in3
-rw-r--r--lib/RT.pm20
-rw-r--r--lib/RT/ACL.pm3
-rw-r--r--lib/RT/Action/CreateTickets.pm13
-rw-r--r--lib/RT/Action/SendEmail.pm9
-rw-r--r--lib/RT/Article.pm11
-rw-r--r--lib/RT/Attachments.pm11
-rw-r--r--lib/RT/Class.pm1
-rw-r--r--lib/RT/CustomField.pm80
-rw-r--r--lib/RT/Dashboard/Mailer.pm3
-rw-r--r--lib/RT/Date.pm30
-rw-r--r--lib/RT/Graph/Tickets.pm10
-rw-r--r--lib/RT/Group.pm10
-rw-r--r--lib/RT/Groups.pm8
-rw-r--r--lib/RT/Handle.pm6
-rw-r--r--lib/RT/Interface/Email.pm27
-rw-r--r--lib/RT/Interface/Web.pm328
-rw-r--r--lib/RT/Interface/Web/Handler.pm10
-rw-r--r--lib/RT/ObjectCustomField.pm12
-rw-r--r--lib/RT/ObjectCustomFieldValue.pm8
-rw-r--r--lib/RT/Queue.pm12
-rw-r--r--lib/RT/Scrip.pm24
-rw-r--r--lib/RT/SearchBuilder.pm13
-rw-r--r--lib/RT/Shredder.pm2
-rw-r--r--lib/RT/Shredder/Plugin.pm1
-rw-r--r--lib/RT/Shredder/Queue.pm1
-rw-r--r--lib/RT/Template.pm24
-rw-r--r--lib/RT/Ticket.pm16
-rw-r--r--lib/RT/Tickets.pm24
-rw-r--r--lib/RT/Transaction.pm18
-rw-r--r--lib/RT/URI.pm2
-rw-r--r--lib/RT/User.pm74
-rw-r--r--lib/RT/Users.pm8
-rwxr-xr-xsbin/rt-server.in1
-rw-r--r--share/html/Admin/Articles/Elements/Topics2
-rw-r--r--share/html/Admin/CustomFields/Modify.html4
-rwxr-xr-xshare/html/Admin/Elements/EditCustomFields3
-rw-r--r--share/html/Admin/Elements/EditRights6
-rw-r--r--share/html/Admin/Elements/Portal2
-rwxr-xr-xshare/html/Admin/Elements/SelectNewGroupMembers8
-rwxr-xr-xshare/html/Admin/Groups/index.html2
-rw-r--r--share/html/Admin/Tools/Queries.html4
-rw-r--r--share/html/Admin/Tools/Shredder/Dumps/dhandler5
-rw-r--r--share/html/Admin/Tools/Shredder/Elements/Error/NoStorage2
-rwxr-xr-xshare/html/Admin/Users/index.html2
-rwxr-xr-xshare/html/Approvals/Elements/PendingMyApproval4
-rw-r--r--share/html/Articles/Article/Edit.html1
-rw-r--r--share/html/Articles/Article/Elements/EditTopics55
-rw-r--r--share/html/Articles/Article/ExtractIntoClass.html2
-rw-r--r--share/html/Articles/Elements/ShowTopicLink27
-rw-r--r--share/html/Articles/Topics.html249
-rw-r--r--share/html/Elements/CSRF74
-rw-r--r--share/html/Elements/CollectionAsTable/Header4
-rw-r--r--share/html/Elements/CollectionListPaging12
-rw-r--r--share/html/Elements/ColumnMap10
-rwxr-xr-xshare/html/Elements/CreateTicket2
-rw-r--r--share/html/Elements/EditCustomField2
-rw-r--r--share/html/Elements/EditCustomFieldAutocomplete13
-rw-r--r--share/html/Elements/EditCustomFieldSelect6
-rwxr-xr-xshare/html/Elements/Error2
-rwxr-xr-xshare/html/Elements/Footer4
-rw-r--r--share/html/Elements/HeaderJavascript4
-rwxr-xr-xshare/html/Elements/MessageBox15
-rw-r--r--share/html/Elements/RT__CustomField/ColumnMap8
-rw-r--r--share/html/Elements/RT__Dashboard/ColumnMap2
-rw-r--r--share/html/Elements/SelectOwnerAutocomplete4
-rw-r--r--share/html/Elements/ShowCustomFields10
-rw-r--r--share/html/Elements/ShowSearch6
-rw-r--r--share/html/Elements/ShowUser2
-rwxr-xr-xshare/html/Elements/Submit14
-rwxr-xr-xshare/html/Elements/Tabs2
-rw-r--r--share/html/Helpers/Autocomplete/CustomFieldValues44
-rw-r--r--share/html/Helpers/Toggle/ShowRequestor4
-rw-r--r--share/html/Install/DatabaseType.html2
-rw-r--r--share/html/Install/Finish.html2
-rw-r--r--share/html/NoAuth/js/titlebox-state.js2
-rw-r--r--share/html/NoAuth/js/userautocomplete.js2
-rw-r--r--share/html/NoAuth/js/util.js4
-rw-r--r--share/html/REST/1.0/Forms/transaction/default3
-rw-r--r--share/html/Search/Chart.html2
-rw-r--r--share/html/Search/Simple.html10
-rwxr-xr-xshare/html/SelfService/Elements/MyRequests22
-rwxr-xr-xshare/html/SelfService/index.html2
-rw-r--r--share/html/Ticket/Elements/Bookmark2
-rw-r--r--share/html/Ticket/Elements/ClickToShowHistory2
-rw-r--r--share/html/Ticket/Elements/FoldStanzaJS2
-rwxr-xr-xshare/html/Ticket/Elements/ShowHistory9
-rwxr-xr-xshare/html/Ticket/Elements/ShowRequestor4
-rw-r--r--share/html/Ticket/Elements/UpdateCc6
-rw-r--r--share/html/Ticket/Graphs/Elements/EditGraphProperties2
-rw-r--r--share/html/Ticket/Graphs/Elements/ShowGraph1
-rw-r--r--share/html/Ticket/Graphs/dhandler1
-rw-r--r--share/html/Widgets/ComboBox4
-rwxr-xr-xshare/html/Widgets/TitleBoxStart2
-rwxr-xr-xshare/html/index.html2
-rwxr-xr-xshare/html/l2
-rwxr-xr-xshare/html/l_unsafe52
-rw-r--r--share/html/m/_elements/footer2
-rw-r--r--share/html/m/ticket/create15
-rw-r--r--share/html/m/ticket/show12
-rw-r--r--share/html/m/tickets/search2
-rw-r--r--t/api/date.t10
-rw-r--r--t/web/case-sensitivity.t2
-rw-r--r--t/web/csrf-rest.t77
-rw-r--r--t/web/csrf.t181
-rw-r--r--t/web/owner_disabled_group_19221.t190
-rw-r--r--t/web/redirect-after-login.t6
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";
diff --git a/lib/RT.pm b/lib/RT.pm
index 4b14d1ef11..063f7f7199 100644
--- a/lib/RT.pm
+++ b/lib/RT.pm
@@ -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" : "").">"
- .("&nbsp;" 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" : "").">"
- .("&nbsp;" 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" : "" %> >\
+<% "&nbsp;" 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" : "" %> >\
+<% "&nbsp;" 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) {
&gt; <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]&nbsp;[_3]</&>
+<&|/l_unsafe, $button_start, $button_end, $queue_selector &>[_1]New ticket in[_2]&nbsp;[_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, '&#187;&#124;&#171;', $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, '&#187;&#124;&#171;', $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> &mdash; ";
+ . qq{onclick="return toggle_all_folds(this, $open_all, $close_all);"}
+ . ">$open_html</a> &mdash; ";
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">&#9660;</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">&#9660;</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");