#
# COPYRIGHT:
#
-# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC
-# <jesse@bestpractical.com>
+# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC
+# <sales@bestpractical.com>
#
# (Except where explicitly superseded by other copyright notices)
#
SendSessionCookie();
$HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new() unless _UserLoggedIn();
+ # Process session-related callbacks before any auth attempts
+ $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Session', CallbackPage => '/autohandler' );
+
+ MaybeRejectPrivateComponentRequest();
+
MaybeShowNoAuthPage($ARGS);
AttemptExternalAuth($ARGS) if RT->Config->Get('WebExternalAuthContinuous') or not _UserLoggedIn();
unless ( _UserLoggedIn() ) {
_ForceLogout();
- # If the user is logging in, let's authenticate
- if ( defined $ARGS->{user} && defined $ARGS->{pass} ) {
- AttemptPasswordAuthentication($ARGS);
- } else {
- # if no credentials then show him login page
- $HTML::Mason::Commands::m->comp( '/Elements/Login', %$ARGS );
- $HTML::Mason::Commands::m->abort;
+ # Authenticate if the user is trying to login via user/pass query args
+ my ($authed, $msg) = AttemptPasswordAuthentication($ARGS);
+
+ unless ($authed) {
+ my $m = $HTML::Mason::Commands::m;
+
+ # REST urls get a special 401 response
+ if ($m->request_comp->path =~ '^/REST/\d+\.\d+/') {
+ $HTML::Mason::Commands::r->content_type("text/plain");
+ $m->error_format("text");
+ $m->out("RT/$RT::VERSION 401 Credentials required\n");
+ $m->out("\n$msg\n") if $msg;
+ $m->abort;
+ }
+ # Specially handle /index.html so that we get a nicer URL
+ elsif ( $m->request_comp->path eq '/index.html' ) {
+ my $next = SetNextPage(RT->Config->Get('WebURL'));
+ $m->comp('/NoAuth/Login.html', next => $next, actions => [$msg]);
+ $m->abort;
+ }
+ else {
+ TangentForLogin(results => ($msg ? LoginError($msg) : undef));
+ }
}
}
ShowRequestedPage($ARGS);
LogRecordedSQLStatements();
+
+ # Process per-page final cleanup callbacks
+ $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Final', CallbackPage => '/autohandler' );
}
sub _ForceLogout {
}
+=head2 LoginError ERROR
+
+Pushes a login error into the Actions session store and returns the hash key.
+
+=cut
+
+sub LoginError {
+ my $new = shift;
+ my $key = Digest::MD5::md5_hex( rand(1024) );
+ push @{ $HTML::Mason::Commands::session{"Actions"}->{$key} ||= [] }, $new;
+ $HTML::Mason::Commands::session{'i'}++;
+ return $key;
+}
+
+=head2 SetNextPage [PATH]
+
+Intuits and stashes the next page in the sesssion hash. If PATH is
+specified, uses that instead of the value of L<IntuitNextPage()>. Returns
+the hash value.
+
+=cut
+
+sub SetNextPage {
+ my $next = shift || IntuitNextPage();
+ my $hash = Digest::MD5::md5_hex($next . $$ . rand(1024));
+
+ $HTML::Mason::Commands::session{'NextPage'}->{$hash} = $next;
+ $HTML::Mason::Commands::session{'i'}++;
+
+ SendSessionCookie();
+ return $hash;
+}
+
+
+=head2 TangentForLogin [HASH]
+
+Redirects to C</NoAuth/Login.html>, setting the value of L<IntuitNextPage> as
+the next page. Optionally takes a hash which is dumped into query params.
+
+=cut
+
+sub TangentForLogin {
+ my $hash = SetNextPage();
+ my %query = (@_, next => $hash);
+ my $login = RT->Config->Get('WebURL') . 'NoAuth/Login.html?';
+ $login .= $HTML::Mason::Commands::m->comp('/Elements/QueryString', %query);
+ Redirect($login);
+}
+
+=head2 TangentForLoginWithError ERROR
+
+Localizes the passed error message, stashes it with L<LoginError> and then
+calls L<TangentForLogin> with the appropriate results key.
+
+=cut
+
+sub TangentForLoginWithError {
+ my $key = LoginError(HTML::Mason::Commands::loc(@_));
+ TangentForLogin( results => $key );
+}
+
+=head2 IntuitNextPage
+
+Attempt to figure out the path to which we should return the user after a
+tangent. The current request URL is used, or failing that, the C<WebURL>
+configuration variable.
+
+=cut
+
+sub IntuitNextPage {
+ my $req_uri;
+
+ # This includes any query parameters. Redirect will take care of making
+ # it an absolute URL.
+ if ($ENV{'REQUEST_URI'}) {
+ $req_uri = $ENV{'REQUEST_URI'};
+
+ # collapse multiple leading slashes so the first part doesn't look like
+ # a hostname of a schema-less URI
+ $req_uri =~ s{^/+}{/};
+ }
+
+ my $next = defined $req_uri ? $req_uri : RT->Config->Get('WebURL');
+
+ # sanitize $next
+ my $uri = URI->new($next);
+
+ # You get undef scheme with a relative uri like "/Search/Build.html"
+ unless (!defined($uri->scheme) || $uri->scheme eq 'http' || $uri->scheme eq 'https') {
+ $next = RT->Config->Get('WebURL');
+ }
+
+ # Make sure we're logging in to the same domain
+ # You can get an undef authority with a relative uri like "index.html"
+ my $uri_base_url = URI->new(RT->Config->Get('WebBaseURL'));
+ unless (!defined($uri->authority) || $uri->authority eq $uri_base_url->authority) {
+ $next = RT->Config->Get('WebURL');
+ }
+
+ return $next;
+}
+
=head2 MaybeShowInstallModePage
This function, called exclusively by RT's autohandler, dispatches
return unless $m->base_comp->path =~ RT->Config->Get('WebNoAuthRegex');
+ # Don't show the login page to logged in users
+ Redirect(RT->Config->Get('WebURL'))
+ if $m->base_comp->path eq '/NoAuth/Login.html' and _UserLoggedIn();
+
# If it's a noauth file, don't ask for auth.
SendSessionCookie();
$m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %$ARGS );
$m->abort;
}
+=head2 MaybeRejectPrivateComponentRequest
+
+This function will reject calls to private components, like those under
+C</Elements>. If the requested path is a private component then we will
+abort with a C<403> error.
+
+=cut
+
+sub MaybeRejectPrivateComponentRequest {
+ my $m = $HTML::Mason::Commands::m;
+ my $path = $m->request_comp->path;
+
+ # We do not check for dhandler here, because requesting our dhandlers
+ # directly is okay. Mason will invoke the dhandler with a dhandler_arg of
+ # 'dhandler'.
+
+ if ($path =~ m{
+ / # leading slash
+ ( Elements |
+ _elements | # mobile UI
+ Widgets |
+ autohandler | # requesting this directly is suspicious
+ l ) # loc component
+ ( $ | / ) # trailing slash or end of path
+ }xi
+ && $path !~ m{ /RTx/Statistics/\w+/Elements/Chart }xi
+ )
+ {
+ warn "rejecting private component $path\n";
+ $m->abort(403);
+ }
+
+ return;
+}
+
=head2 ShowRequestedPage \%ARGS
This function, called exclusively by RT's autohandler, dispatches
# we failed to successfully create the user. abort abort abort.
delete $HTML::Mason::Commands::session{'CurrentUser'};
- $m->comp( '/Elements/Login', %$ARGS, Error => HTML::Mason::Commands::loc( 'Cannot create user: [_1]', $msg ) )
- if RT->Config->Get('WebFallbackToInternalAuth');;
- $m->abort();
+
+ if (RT->Config->Get('WebFallbackToInternalAuth')) {
+ TangentForLoginWithError('Cannot create user: [_1]', $msg);
+ } else {
+ $m->abort();
+ }
}
}
$user = $orig_user;
if ( RT->Config->Get('WebExternalOnly') ) {
- $m->comp( '/Elements/Login', %$ARGS, Error => HTML::Mason::Commands::loc('You are not an authorized user') );
- $m->abort();
+ TangentForLoginWithError('You are not an authorized user');
}
}
} elsif ( RT->Config->Get('WebFallbackToInternalAuth') ) {
unless ( defined $HTML::Mason::Commands::session{'CurrentUser'} ) {
# XXX unreachable due to prior defaulting in HandleRequest (check c34d108)
- $m->comp( '/Elements/Login', %$ARGS, Error => HTML::Mason::Commands::loc('You are not an authorized user') );
- $m->abort();
+ TangentForLoginWithError('You are not an authorized user');
}
} else {
}
sub AttemptPasswordAuthentication {
- my $ARGS = shift;
+ my $ARGS = shift;
+ return unless defined $ARGS->{user} && defined $ARGS->{pass};
+
my $user_obj = RT::CurrentUser->new();
$user_obj->Load( $ARGS->{user} );
unless ( $user_obj->id && $user_obj->IsPassword( $ARGS->{pass} ) ) {
$RT::Logger->error("FAILED LOGIN for @{[$ARGS->{user}]} from $ENV{'REMOTE_ADDR'}");
- $m->comp( '/Elements/Login', %$ARGS, Error => HTML::Mason::Commands::loc('Your username or password is incorrect'), );
$m->callback( %$ARGS, CallbackName => 'FailedLogin', CallbackPage => '/autohandler' );
- $m->abort;
+ return (0, HTML::Mason::Commands::loc('Your username or password is incorrect'));
}
+ else {
+ $RT::Logger->info("Successful login for @{[$ARGS->{user}]} from $ENV{'REMOTE_ADDR'}");
+
+ # It's important to nab the next page from the session before we blow
+ # the session away
+ my $next = delete $HTML::Mason::Commands::session{'NextPage'}->{$ARGS->{'next'} || ''};
+
+ InstantiateNewSession();
+ $HTML::Mason::Commands::session{'CurrentUser'} = $user_obj;
+ SendSessionCookie();
- $RT::Logger->info("Successful login for @{[$ARGS->{user}]} from $ENV{'REMOTE_ADDR'}");
- InstantiateNewSession();
- $HTML::Mason::Commands::session{'CurrentUser'} = $user_obj;
- $m->callback( %$ARGS, CallbackName => 'SuccessfulLogin', CallbackPage => '/autohandler' );
+ $m->callback( %$ARGS, CallbackName => 'SuccessfulLogin', CallbackPage => '/autohandler' );
+
+ # Really the only time we don't want to redirect here is if we were
+ # passed user and pass as query params in the URL.
+ if ($next) {
+ Redirect($next);
+ }
+ elsif ($ARGS->{'next'}) {
+ # Invalid hash, but still wants to go somewhere, take them to /
+ Redirect(RT->Config->Get('WebURL'));
+ }
+
+ return (1, HTML::Mason::Commands::loc('Logged in'));
+ }
}
=head2 LoadSessionFromCookie
untie $HTML::Mason::Commands::session;
my $uri = URI->new($redir_to);
my $server_uri = URI->new( RT->Config->Get('WebURL') );
+
+ # Make relative URIs absolute from the server host and scheme
+ $uri->scheme($server_uri->scheme) if not defined $uri->scheme;
+ if (not defined $uri->host) {
+ $uri->host($server_uri->host);
+ $uri->port($server_uri->port);
+ }
# If the user is coming in via a non-canonical
# hostname, don't redirect them to the canonical host,
# $HTML::Mason::Commands::r->headers_out->{'Last-Modified'} = $date->RFC2616;
}
+=head2 PathIsSafe
+
+Takes a C<< Path => path >> and returns a boolean indicating that
+the path is safely within RT's control or not. The path I<must> be
+relative.
+
+This function does not consult the filesystem at all; it is merely
+a logical sanity checking of the path. This explicitly does not handle
+symlinks; if you have symlinks in RT's webroot pointing outside of it,
+then we assume you know what you are doing.
+
+=cut
+
+sub PathIsSafe {
+ my $self = shift;
+ my %args = @_;
+ my $path = $args{Path};
+
+ # Get File::Spec to clean up extra /s, ./, etc
+ my $cleaned_up = File::Spec->canonpath($path);
+
+ if (!defined($cleaned_up)) {
+ $RT::Logger->info("Rejecting path that canonpath doesn't understand: $path");
+ return 0;
+ }
+
+ # Forbid too many ..s. We can't just sum then check because
+ # "../foo/bar/baz" should be illegal even though it has more
+ # downdirs than updirs. So as soon as we get a negative score
+ # (which means "breaking out" of the top level) we reject the path.
+
+ my @components = split '/', $cleaned_up;
+ my $score = 0;
+ for my $component (@components) {
+ if ($component eq '..') {
+ $score--;
+ if ($score < 0) {
+ $RT::Logger->info("Rejecting unsafe path: $path");
+ return 0;
+ }
+ }
+ elsif ($component eq '.' || $component eq '') {
+ # these two have no effect on $score
+ }
+ else {
+ $score++;
+ }
+ }
+
+ return 1;
+}
+
=head2 SendStaticFile
Takes a File => path and a Type => Content-type
my %args = @_;
my $file = $args{File};
my $type = $args{Type};
+ my $relfile = $args{RelativeFile};
+
+ if (defined($relfile) && !$self->PathIsSafe(Path => $relfile)) {
+ $HTML::Mason::Commands::r->status(400);
+ $HTML::Mason::Commands::m->abort;
+ }
$self->StaticFileHeaders();
}
$type ||= "application/octet-stream";
}
+
+ # CGI.pm version 3.51 and 3.52 bang charset=iso-8859-1 onto our JS
+ # since we don't specify a charset
+ if ( $type =~ m{application/javascript} &&
+ $type !~ m{charset=([\w-]+)$} ) {
+ $type .= "; charset=utf-8";
+ }
$HTML::Mason::Commands::r->content_type($type);
- open my $fh, "<$file" or die "couldn't open file: $!";
+ open( my $fh, '<', $file ) or die "couldn't open file: $!";
binmode($fh);
{
local $/ = \16384;
# Check for plaintext sig
return '' if not $html and $content =~ /^(--)?\Q$sig\E$/;
- # Check for html-formatted sig
- RT::Interface::Web::EscapeUTF8( \$sig );
+ # Check for html-formatted sig; we don't use EscapeUTF8 here
+ # because we want to precisely match the escaping that FCKEditor
+ # uses. see also 311223f5, which fixed this for 4.0
+ $sig =~ s/&/&/g;
+ $sig =~ s/</</g;
+ $sig =~ s/>/>/g;
+
return ''
if $html
and $content =~ m{^(?:<p>)?(--)?\Q$sig\E(?:</p>)?$}s;
}
foreach my $argument (qw(Encrypt Sign)) {
- $MIMEObj->head->add( "X-RT-$argument" => $ARGS{$argument} ) if defined $ARGS{$argument};
+ $MIMEObj->head->add(
+ "X-RT-$argument" => Encode::encode_utf8( $ARGS{$argument} )
+ ) if defined $ARGS{$argument};
}
my %create_args = (
Type => $args{ARGSRef}->{'UpdateContentType'},
);
- $Message->head->add( 'Message-ID' => RT::Interface::Email::GenMessageId( Ticket => $args{'TicketObj'}, ) );
+ $Message->head->add( 'Message-ID' => Encode::encode_utf8(
+ RT::Interface::Email::GenMessageId( Ticket => $args{'TicketObj'} )
+ ) );
my $old_txn = RT::Transaction->new( $session{'CurrentUser'} );
if ( $args{ARGSRef}->{'QuoteTransaction'} ) {
$old_txn->Load( $args{ARGSRef}->{'QuoteTransaction'} );
TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'}
);
+ my @temp_squelch;
+ foreach my $type (qw(Cc AdminCc)) {
+ if (grep $_ eq $type || $_ eq ( $type . 's' ), @{ $args{ARGSRef}->{'SkipNotification'} || [] }) {
+ push @temp_squelch, map $_->address, Email::Address->parse( $message_args{$type} );
+ push @temp_squelch, $args{TicketObj}->$type->MemberEmailAddresses;
+ push @temp_squelch, $args{TicketObj}->QueueObj->$type->MemberEmailAddresses;
+ }
+ }
+ if (grep $_ eq 'Requestor' || $_ eq 'Requestors', @{ $args{ARGSRef}->{'SkipNotification'} || [] }) {
+ push @temp_squelch, map $_->address, Email::Address->parse( $message_args{Requestor} );
+ push @temp_squelch, $args{TicketObj}->Requestors->MemberEmailAddresses;
+ }
+
+ if (@temp_squelch) {
+ require RT::Action::SendEmail;
+ RT::Action::SendEmail->SquelchMailTo( RT::Action::SendEmail->SquelchMailTo, @temp_squelch );
+ }
+
unless ( $args{'ARGSRef'}->{'UpdateIgnoreAddressCheckboxes'} ) {
foreach my $key ( keys %{ $args{ARGSRef} } ) {
next unless $key =~ /^Update(Cc|Bcc)-(.*)$/;
);
my $Message = MIME::Entity->build(
Type => 'multipart/mixed',
- Subject => $args{'Subject'} || "",
- From => $args{'From'},
- Cc => $args{'Cc'},
+ map { $_ => Encode::encode_utf8( $args{ $_} ) }
+ grep defined $args{$_}, qw(Subject From Cc)
);
if ( defined $args{'Body'} && length $args{'Body'} ) {
# Make the update content have no 'weird' newlines in it
$args{'Body'} =~ s/\r\n/\n/gs;
- # MIME::Head is not happy in utf-8 domain. This only happens
- # when processing an incoming email (so far observed).
- no utf8;
- use bytes;
$Message->attach(
- Type => $args{'Type'} || 'text/plain',
+ Type => $args{'Type'} || 'text/plain',
Charset => 'UTF-8',
Data => $args{'Body'},
);
# Prefer the cached name first over CGI.pm stringification.
my $filename = $RT::Mason::CGI::Filename;
- $filename = "$filehandle" unless defined($filename);
- $filename = Encode::decode_utf8($filename);
+ $filename = "$filehandle" unless defined $filename;
+ $filename = Encode::encode_utf8( $filename );
$filename =~ s{^.*[\\/]}{};
$Message->attach(
}
$Message->make_singlepart;
+
RT::I18N::SetMIMEEntityToUTF8($Message); # convert text parts into utf-8
return ($Message);
sub ProcessACLChanges {
my $ARGSref = shift;
- #XXX: why don't we get ARGSref like in other Process* subs?
-
my @results;
foreach my $arg ( keys %$ARGSref ) {
$values_hash{$val} = 1 if $val;
}
+ # For Date Cfs, @values is empty when there is no changes (no datas in form input)
+ return @results if ( $cf->Type eq 'Date' && ! @values );
+
$cf_values->RedoSearch;
while ( my $cf_value = $cf_values->Next ) {
next if $values_hash{ $cf_value->id };
return ( _load_container_object( $obj_type, $obj_id ), $search_id );
}
-eval "require RT::Interface::Web_Vendor";
-die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Interface/Web_Vendor.pm} );
-eval "require RT::Interface::Web_Local";
-die $@ if ( $@ && $@ !~ qr{^Can't locate RT/Interface/Web_Local.pm} );
+RT::Base->_ImportOverlays();
1;