X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=rt%2Flib%2FRT%2FInterface%2FWeb.pm;h=959c80334e73a45d1782a5846d79e61df1e5f6a6;hb=8a8f0b0872dcc208b6048255c1dc0d9d9ecf8088;hp=106209d64513357f1012f50c1df57c82762484a0;hpb=c587b5fdc7175c2a752558efccfc3f424cff6c0d;p=freeside.git diff --git a/rt/lib/RT/Interface/Web.pm b/rt/lib/RT/Interface/Web.pm index 106209d64..959c80334 100644 --- a/rt/lib/RT/Interface/Web.pm +++ b/rt/lib/RT/Interface/Web.pm @@ -2,8 +2,8 @@ # # COPYRIGHT: # -# This software is Copyright (c) 1996-2009 Best Practical Solutions, LLC -# +# This software is Copyright (c) 1996-2011 Best Practical Solutions, LLC +# # # (Except where explicitly superseded by other copyright notices) # @@ -192,6 +192,11 @@ sub HandleRequest { 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(); @@ -204,13 +209,29 @@ sub HandleRequest { 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)); + } } } @@ -223,6 +244,9 @@ sub HandleRequest { ShowRequestedPage($ARGS); LogRecordedSQLStatements(); + + # Process per-page final cleanup callbacks + $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Final', CallbackPage => '/autohandler' ); } sub _ForceLogout { @@ -239,6 +263,108 @@ sub _UserLoggedIn { } +=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. 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, setting the value of L 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 and then +calls L 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 +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 @@ -278,12 +404,51 @@ sub MaybeShowNoAuthPage { 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. 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 @@ -380,9 +545,12 @@ sub AttemptExternalAuth { # 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(); + } } } @@ -393,15 +561,13 @@ sub AttemptExternalAuth { $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 { @@ -414,7 +580,9 @@ sub AttemptExternalAuth { } 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} ); @@ -422,15 +590,34 @@ sub AttemptPasswordAuthentication { 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(); + + $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')); + } - $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' ); + return (1, HTML::Mason::Commands::loc('Logged in')); + } } =head2 LoadSessionFromCookie @@ -497,6 +684,13 @@ sub Redirect { 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, @@ -639,8 +833,15 @@ sub SendStaticFile { } $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; @@ -684,8 +885,13 @@ sub StripContent { # 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; + return '' if $html and $content =~ m{^(?:

)?(--)?\Q$sig\E(?:

)?$}s; @@ -1177,13 +1383,22 @@ sub ProcessUpdateMessage { my $bcc = $args{ARGSRef}->{'UpdateBcc'}; my $cc = $args{ARGSRef}->{'UpdateCc'}; + my %txn_customfields; + + foreach my $key ( keys %{ $args{ARGSRef} } ) { + if ( $key =~ /^(?:Object-RT::Transaction--)?CustomField-(\d+)/ ) { + $txn_customfields{$key} = $args{ARGSRef}->{$key}; + } + } + my %message_args = ( CcMessageTo => $cc, BccMessageTo => $bcc, Sign => $args{ARGSRef}->{'Sign'}, Encrypt => $args{ARGSRef}->{'Encrypt'}, MIMEObj => $Message, - TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'} + TimeTaken => $args{ARGSRef}->{'UpdateTimeWorked'}, + CustomFields => \%txn_customfields, ); my @temp_squelch; @@ -1219,14 +1434,17 @@ sub ProcessUpdateMessage { } my @results; + # Do the update via the appropriate Ticket method if ( $args{ARGSRef}->{'UpdateType'} =~ /^(private|public)$/ ) { - my ( $Transaction, $Description, $Object ) = $args{TicketObj}->Comment(%message_args); + my ( $Transaction, $Description, $Object ) = + $args{TicketObj}->Comment(%message_args); push( @results, $Description ); - $Object->UpdateCustomFields( ARGSRef => $args{ARGSRef} ) if $Object; + #$Object->UpdateCustomFields( ARGSRef => $args{ARGSRef} ) if $Object; } elsif ( $args{ARGSRef}->{'UpdateType'} eq 'response' ) { - my ( $Transaction, $Description, $Object ) = $args{TicketObj}->Correspond(%message_args); + my ( $Transaction, $Description, $Object ) = + $args{TicketObj}->Correspond(%message_args); push( @results, $Description ); - $Object->UpdateCustomFields( ARGSRef => $args{ARGSRef} ) if $Object; + #$Object->UpdateCustomFields( ARGSRef => $args{ARGSRef} ) if $Object; } else { push( @results, loc("Update type was neither correspondence nor comment.") . " " . loc("Update not recorded.") ); @@ -1559,6 +1777,8 @@ sub ProcessTicketCustomFieldUpdates { $ARGSRef->{"Object-RT::Ticket-$1"} = delete $ARGSRef->{$arg}; } elsif ( $arg =~ /^CustomField-(\d+-.*)/ ) { $ARGSRef->{"Object-RT::Ticket--$1"} = delete $ARGSRef->{$arg}; + } elsif ( $arg =~ /^Object-RT::Transaction-(\d*)-CustomField/ ) { + delete $ARGSRef->{$arg}; # don't try to update transaction fields } } @@ -1634,6 +1854,9 @@ sub _ProcessObjectCustomFieldUpdates { # skip category argument next if $arg eq 'Category'; + # and TimeUnits + next if $arg eq 'Value-TimeUnits'; + # since http won't pass in a form element with a null value, we need # to fake it if ( $arg eq 'Values-Magic' ) { @@ -2098,9 +2321,7 @@ sub _parse_saved_search { 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} ); +package RT::Interface::Web; +RT::Base->_ImportOverlays(); 1;