Skip to content

Commit 0575522

Browse files
committed
Merge branch 'redirect-after-login' into 3.8-trunk
2 parents 432df1d + d37f3e9 commit 0575522

File tree

6 files changed

+484
-86
lines changed

6 files changed

+484
-86
lines changed

Diff for: lib/RT/Interface/Web.pm

+172-21
Original file line numberDiff line numberDiff line change
@@ -207,13 +207,29 @@ sub HandleRequest {
207207
unless ( _UserLoggedIn() ) {
208208
_ForceLogout();
209209

210-
# If the user is logging in, let's authenticate
211-
if ( defined $ARGS->{user} && defined $ARGS->{pass} ) {
212-
AttemptPasswordAuthentication($ARGS);
213-
} else {
214-
# if no credentials then show him login page
215-
$HTML::Mason::Commands::m->comp( '/Elements/Login', %$ARGS );
216-
$HTML::Mason::Commands::m->abort;
210+
# Authenticate if the user is trying to login via user/pass query args
211+
my ($authed, $msg) = AttemptPasswordAuthentication($ARGS);
212+
213+
unless ($authed) {
214+
my $m = $HTML::Mason::Commands::m;
215+
216+
# REST urls get a special 401 response
217+
if ($m->request_comp->path =~ '^/REST/\d+\.\d+/') {
218+
$HTML::Mason::Commands::r->content_type("text/plain");
219+
$m->error_format("text");
220+
$m->out("RT/$RT::VERSION 401 Credentials required\n");
221+
$m->out("\n$msg\n") if $msg;
222+
$m->abort;
223+
}
224+
# Specially handle /index.html so that we get a nicer URL
225+
elsif ( $m->request_comp->path eq '/index.html' ) {
226+
my $next = SetNextPage(RT->Config->Get('WebURL'));
227+
$m->comp('/NoAuth/Login.html', next => $next, actions => [$msg]);
228+
$m->abort;
229+
}
230+
else {
231+
TangentForLogin(results => ($msg ? LoginError($msg) : undef));
232+
}
217233
}
218234
}
219235

@@ -245,6 +261,108 @@ sub _UserLoggedIn {
245261

246262
}
247263

264+
=head2 LoginError ERROR
265+
266+
Pushes a login error into the Actions session store and returns the hash key.
267+
268+
=cut
269+
270+
sub LoginError {
271+
my $new = shift;
272+
my $key = Digest::MD5::md5_hex( rand(1024) );
273+
push @{ $HTML::Mason::Commands::session{"Actions"}->{$key} ||= [] }, $new;
274+
$HTML::Mason::Commands::session{'i'}++;
275+
return $key;
276+
}
277+
278+
=head2 SetNextPage [PATH]
279+
280+
Intuits and stashes the next page in the sesssion hash. If PATH is
281+
specified, uses that instead of the value of L<IntuitNextPage()>. Returns
282+
the hash value.
283+
284+
=cut
285+
286+
sub SetNextPage {
287+
my $next = shift || IntuitNextPage();
288+
my $hash = Digest::MD5::md5_hex($next . $$ . rand(1024));
289+
290+
$HTML::Mason::Commands::session{'NextPage'}->{$hash} = $next;
291+
$HTML::Mason::Commands::session{'i'}++;
292+
293+
SendSessionCookie();
294+
return $hash;
295+
}
296+
297+
298+
=head2 TangentForLogin [HASH]
299+
300+
Redirects to C</NoAuth/Login.html>, setting the value of L<IntuitNextPage> as
301+
the next page. Optionally takes a hash which is dumped into query params.
302+
303+
=cut
304+
305+
sub TangentForLogin {
306+
my $hash = SetNextPage();
307+
my %query = (@_, next => $hash);
308+
my $login = RT->Config->Get('WebURL') . 'NoAuth/Login.html?';
309+
$login .= $HTML::Mason::Commands::m->comp('/Elements/QueryString', %query);
310+
Redirect($login);
311+
}
312+
313+
=head2 TangentForLoginWithError ERROR
314+
315+
Localizes the passed error message, stashes it with L<LoginError> and then
316+
calls L<TangentForLogin> with the appropriate results key.
317+
318+
=cut
319+
320+
sub TangentForLoginWithError {
321+
my $key = LoginError(HTML::Mason::Commands::loc(@_));
322+
TangentForLogin( results => $key );
323+
}
324+
325+
=head2 IntuitNextPage
326+
327+
Attempt to figure out the path to which we should return the user after a
328+
tangent. The current request URL is used, or failing that, the C<WebURL>
329+
configuration variable.
330+
331+
=cut
332+
333+
sub IntuitNextPage {
334+
my $req_uri;
335+
336+
# This includes any query parameters. Redirect will take care of making
337+
# it an absolute URL.
338+
if ($ENV{'REQUEST_URI'}) {
339+
$req_uri = $ENV{'REQUEST_URI'};
340+
341+
# collapse multiple leading slashes so the first part doesn't look like
342+
# a hostname of a schema-less URI
343+
$req_uri =~ s{^/+}{/};
344+
}
345+
346+
my $next = defined $req_uri ? $req_uri : RT->Config->Get('WebURL');
347+
348+
# sanitize $next
349+
my $uri = URI->new($next);
350+
351+
# You get undef scheme with a relative uri like "/Search/Build.html"
352+
unless (!defined($uri->scheme) || $uri->scheme eq 'http' || $uri->scheme eq 'https') {
353+
$next = RT->Config->Get('WebURL');
354+
}
355+
356+
# Make sure we're logging in to the same domain
357+
# You can get an undef authority with a relative uri like "index.html"
358+
my $uri_base_url = URI->new(RT->Config->Get('WebBaseURL'));
359+
unless (!defined($uri->authority) || $uri->authority eq $uri_base_url->authority) {
360+
$next = RT->Config->Get('WebURL');
361+
}
362+
363+
return $next;
364+
}
365+
248366
=head2 MaybeShowInstallModePage
249367
250368
This function, called exclusively by RT's autohandler, dispatches
@@ -284,6 +402,10 @@ sub MaybeShowNoAuthPage {
284402

285403
return unless $m->base_comp->path =~ RT->Config->Get('WebNoAuthRegex');
286404

405+
# Don't show the login page to logged in users
406+
Redirect(RT->Config->Get('WebURL'))
407+
if $m->base_comp->path eq '/NoAuth/Login.html' and _UserLoggedIn();
408+
287409
# If it's a noauth file, don't ask for auth.
288410
SendSessionCookie();
289411
$m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %$ARGS );
@@ -386,9 +508,12 @@ sub AttemptExternalAuth {
386508

387509
# we failed to successfully create the user. abort abort abort.
388510
delete $HTML::Mason::Commands::session{'CurrentUser'};
389-
$m->comp( '/Elements/Login', %$ARGS, Error => HTML::Mason::Commands::loc( 'Cannot create user: [_1]', $msg ) )
390-
if RT->Config->Get('WebFallbackToInternalAuth');;
391-
$m->abort();
511+
512+
if (RT->Config->Get('WebFallbackToInternalAuth')) {
513+
TangentForLoginWithError('Cannot create user: [_1]', $msg);
514+
} else {
515+
$m->abort();
516+
}
392517
}
393518
}
394519

@@ -399,15 +524,13 @@ sub AttemptExternalAuth {
399524
$user = $orig_user;
400525

401526
if ( RT->Config->Get('WebExternalOnly') ) {
402-
$m->comp( '/Elements/Login', %$ARGS, Error => HTML::Mason::Commands::loc('You are not an authorized user') );
403-
$m->abort();
527+
TangentForLoginWithError('You are not an authorized user');
404528
}
405529
}
406530
} elsif ( RT->Config->Get('WebFallbackToInternalAuth') ) {
407531
unless ( defined $HTML::Mason::Commands::session{'CurrentUser'} ) {
408532
# XXX unreachable due to prior defaulting in HandleRequest (check c34d108)
409-
$m->comp( '/Elements/Login', %$ARGS, Error => HTML::Mason::Commands::loc('You are not an authorized user') );
410-
$m->abort();
533+
TangentForLoginWithError('You are not an authorized user');
411534
}
412535
} else {
413536

@@ -420,23 +543,44 @@ sub AttemptExternalAuth {
420543
}
421544

422545
sub AttemptPasswordAuthentication {
423-
my $ARGS = shift;
546+
my $ARGS = shift;
547+
return unless defined $ARGS->{user} && defined $ARGS->{pass};
548+
424549
my $user_obj = RT::CurrentUser->new();
425550
$user_obj->Load( $ARGS->{user} );
426551

427552
my $m = $HTML::Mason::Commands::m;
428553

429554
unless ( $user_obj->id && $user_obj->IsPassword( $ARGS->{pass} ) ) {
430555
$RT::Logger->error("FAILED LOGIN for @{[$ARGS->{user}]} from $ENV{'REMOTE_ADDR'}");
431-
$m->comp( '/Elements/Login', %$ARGS, Error => HTML::Mason::Commands::loc('Your username or password is incorrect'), );
432556
$m->callback( %$ARGS, CallbackName => 'FailedLogin', CallbackPage => '/autohandler' );
433-
$m->abort;
557+
return (0, HTML::Mason::Commands::loc('Your username or password is incorrect'));
434558
}
559+
else {
560+
$RT::Logger->info("Successful login for @{[$ARGS->{user}]} from $ENV{'REMOTE_ADDR'}");
561+
562+
# It's important to nab the next page from the session before we blow
563+
# the session away
564+
my $next = delete $HTML::Mason::Commands::session{'NextPage'}->{$ARGS->{'next'} || ''};
435565

436-
$RT::Logger->info("Successful login for @{[$ARGS->{user}]} from $ENV{'REMOTE_ADDR'}");
437-
InstantiateNewSession();
438-
$HTML::Mason::Commands::session{'CurrentUser'} = $user_obj;
439-
$m->callback( %$ARGS, CallbackName => 'SuccessfulLogin', CallbackPage => '/autohandler' );
566+
InstantiateNewSession();
567+
$HTML::Mason::Commands::session{'CurrentUser'} = $user_obj;
568+
SendSessionCookie();
569+
570+
$m->callback( %$ARGS, CallbackName => 'SuccessfulLogin', CallbackPage => '/autohandler' );
571+
572+
# Really the only time we don't want to redirect here is if we were
573+
# passed user and pass as query params in the URL.
574+
if ($next) {
575+
Redirect($next);
576+
}
577+
elsif ($ARGS->{'next'}) {
578+
# Invalid hash, but still wants to go somewhere, take them to /
579+
Redirect(RT->Config->Get('WebURL'));
580+
}
581+
582+
return (1, HTML::Mason::Commands::loc('Logged in'));
583+
}
440584
}
441585

442586
=head2 LoadSessionFromCookie
@@ -503,6 +647,13 @@ sub Redirect {
503647
untie $HTML::Mason::Commands::session;
504648
my $uri = URI->new($redir_to);
505649
my $server_uri = URI->new( RT->Config->Get('WebURL') );
650+
651+
# Make relative URIs absolute from the server host and scheme
652+
$uri->scheme($server_uri->scheme) if not defined $uri->scheme;
653+
if (not defined $uri->host) {
654+
$uri->host($server_uri->host);
655+
$uri->port($server_uri->port);
656+
}
506657

507658
# If the user is coming in via a non-canonical
508659
# hostname, don't redirect them to the canonical host,

Diff for: share/html/Elements/ListActions

+2-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
%#
4747
%# END BPS TAGGED BLOCK }}}
4848
<div class="results">
49-
<&| /Widgets/TitleBox, title => loc('Results') &>
49+
<&| /Widgets/TitleBox, title => loc('Results'), %{$titlebox || {}} &>
5050
<ul class="action-results">
5151
% foreach my $action (@actions) {
5252
<li><%$action%></li>
@@ -90,5 +90,6 @@ return unless @actions;
9090

9191
</%init>
9292
<%ARGS>
93+
$titlebox => {}
9394
@actions => undef
9495
</%ARGS>

Diff for: share/html/Elements/Login

+11-64
Original file line numberDiff line numberDiff line change
@@ -45,42 +45,6 @@
4545
%# those contributions and any derivatives thereof.
4646
%#
4747
%# END BPS TAGGED BLOCK }}}
48-
<%INIT>
49-
if ($m->request_comp->path =~ '^/REST/\d+\.\d+/') {
50-
$r->content_type("text/plain");
51-
$m->error_format("text");
52-
$m->out("RT/$RT::VERSION 401 Credentials required\n");
53-
$m->out("\n$Error\n") if $Error;
54-
$m->abort;
55-
}
56-
57-
my $req_uri;
58-
59-
if (UNIVERSAL::can($r, 'uri') and $r->uri =~ m{.*/(.*)}) {
60-
$req_uri = $1;
61-
}
62-
63-
my $form_action = defined $goto ? $goto
64-
: defined $req_uri ? $req_uri
65-
: RT->Config->Get('WebPath')
66-
;
67-
68-
# sanitize $form_action
69-
my $uri = URI->new($form_action);
70-
71-
# You get undef scheme with a relative uri like "/Search/Build.html"
72-
unless (!defined($uri->scheme) || $uri->scheme eq 'http' || $uri->scheme eq 'https') {
73-
$form_action = RT->Config->Get('WebPath');
74-
}
75-
76-
# Make sure we're logging in to the same domain
77-
# You can get an undef authority with a relative uri like "index.html"
78-
my $uri_base_url = URI->new(RT->Config->Get('WebBaseURL'));
79-
unless (!defined($uri->authority) || $uri->authority eq $uri_base_url->authority) {
80-
$form_action = RT->Config->Get('WebPath');
81-
}
82-
</%INIT>
83-
8448
% $m->callback( %ARGS, CallbackName => 'Header' );
8549
<& /Elements/Header, Title => loc('Login'), Focus => 'user' &>
8650

@@ -89,19 +53,20 @@ unless (!defined($uri->authority) || $uri->authority eq $uri_base_url->authority
8953
</div>
9054

9155
<div id="body" class="login-body">
92-
% if ($Error) {
93-
<&| "/Widgets/TitleBox", title => loc('Error'), hideable => 0, class => 'error' &>
94-
<% $Error %>
95-
</&>
96-
% }
56+
57+
<& /Elements/ListActions,
58+
title => loc('Error'),
59+
titlebox => { class => 'error', hideable => 0 },
60+
actions => $actions
61+
&>
9762

9863
% $m->callback( %ARGS, CallbackName => 'BeforeForm' );
9964

10065
<div id="login-box">
10166
<&| /Widgets/TitleBox, title => loc('Login'), titleright => $RT::VERSION, hideable => 0 &>
10267

10368
% unless (RT->Config->Get('WebExternalAuth') and !RT->Config->Get('WebFallbackToInternalAuth')) {
104-
<form id="login" name="login" method="post" action="<% $form_action %>">
69+
<form id="login" name="login" method="post" action="<% RT->Config->Get('WebPath') %>/NoAuth/Login.html">
10570

10671
<div class="input-row">
10772
<span class="label"><&|/l&>Username</&>:</span>
@@ -113,32 +78,15 @@ unless (!defined($uri->authority) || $uri->authority eq $uri_base_url->authority
11378
<span class="input"><input type="password" name="pass" autocomplete="off" /></span>
11479
</div>
11580

81+
<input type="hidden" name="next" value="<% $next %>" />
82+
11683
<div class="button-row">
11784
<span class="input"><input type="submit" class="button" value="<&|/l&>Login</&>" /></span>
11885
</div>
11986

12087
%# Give callbacks a chance to add more control elements
12188
% $m->callback( %ARGS );
12289

123-
% # From mason 1.0.1 forward, this doesn't work. in fact, it breaks things.
124-
% # But on Mason 1.15 it's fixed again, so we still use it.
125-
% # The code below iterates through everything in the passed in arguments
126-
% # Preserving all the old parameters
127-
% # This would be easier, except mason is 'smart' and calls multiple values
128-
% # arrays rather than multiple hash keys
129-
% my $key; my $val;
130-
% foreach $key (keys %ARGS) {
131-
% if (($key ne 'user') and ($key ne 'pass')) {
132-
% if (ref($ARGS{$key}) =~ /ARRAY/) {
133-
% foreach $val (@{$ARGS{$key}}) {
134-
<input type="hidden" class="hidden" name="<%$key %>" value="<% $val %>" />
135-
% }
136-
% }
137-
% else {
138-
<input type="hidden" class="hidden" name="<% $key %>" value="<% $ARGS{$key} %>" />
139-
% }
140-
% }
141-
% }
14290
</form>
14391
% }
14492
</&>
@@ -147,8 +95,7 @@ unless (!defined($uri->authority) || $uri->authority eq $uri_base_url->authority
14795
</div><!-- #login-body -->
14896
<& /Elements/Footer, Menu => 0 &>
14997
<%ARGS>
98+
$next => ''
15099
$user => ""
151-
$pass => undef
152-
$goto => undef
153-
$Error => undef
100+
$actions => undef
154101
</%ARGS>

0 commit comments

Comments
 (0)