+sub EscapeUTF8 {
+ my $ref = shift;
+ return unless defined $$ref;
+
+ $$ref =~ s/&/&/g;
+ $$ref =~ s/</</g;
+ $$ref =~ s/>/>/g;
+ $$ref =~ s/\(/(/g;
+ $$ref =~ s/\)/)/g;
+ $$ref =~ s/"/"/g;
+ $$ref =~ s/'/'/g;
+}
+
+
+
+=head2 EscapeURI SCALARREF
+
+Escapes URI component according to RFC2396
+
+=cut
+
+sub EscapeURI {
+ my $ref = shift;
+ return unless defined $$ref;
+
+ use bytes;
+ $$ref =~ s/([^a-zA-Z0-9_.!~*'()-])/uc sprintf("%%%02X", ord($1))/eg;
+}
+
+=head2 EncodeJSON SCALAR
+
+Encodes the SCALAR to JSON and returns a JSON string. SCALAR may be a simple
+value or a reference.
+
+=cut
+
+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
+function must return something suitable for REMOTE_USER. By default,
+just downcase $ENV{'REMOTE_USER'}
+
+=cut
+
+sub WebCanonicalizeInfo {
+ return $ENV{'REMOTE_USER'} ? lc $ENV{'REMOTE_USER'} : $ENV{'REMOTE_USER'};
+}
+
+
+
+=head2 WebExternalAutoInfo($user);
+
+Returns a hash of user attributes, used when WebExternalAuto is set.
+
+=cut
+
+sub WebExternalAutoInfo {
+ my $user = shift;
+
+ my %user_info;
+
+ # default to making Privileged users, even if they specify
+ # some other default Attributes
+ if ( !$RT::AutoCreate
+ || ( ref($RT::AutoCreate) && not exists $RT::AutoCreate->{Privileged} ) )
+ {
+ $user_info{'Privileged'} = 1;
+ }
+
+ if ( $^O !~ /^(?:riscos|MacOS|MSWin32|dos|os2)$/ ) {
+
+ # Populate fields with information from Unix /etc/passwd
+
+ my ( $comments, $realname ) = ( getpwnam($user) )[ 5, 6 ];
+ $user_info{'Comments'} = $comments if defined $comments;
+ $user_info{'RealName'} = $realname if defined $realname;
+ } elsif ( $^O eq 'MSWin32' and eval 'use Net::AdminMisc; 1' ) {
+
+ # Populate fields with information from NT domain controller
+ }
+
+ # and return the wad of stuff
+ return {%user_info};
+}
+
+
+sub HandleRequest {
+ my $ARGS = shift;
+
+ if (RT->Config->Get('DevelMode')) {
+ require Module::Refresh;
+ Module::Refresh->refresh;
+ }
+
+ $HTML::Mason::Commands::r->content_type("text/html; charset=utf-8");
+
+ $HTML::Mason::Commands::m->{'rt_base_time'} = [ Time::HiRes::gettimeofday() ];
+
+ # Roll back any dangling transactions from a previous failed connection
+ $RT::Handle->ForceRollback() if $RT::Handle and $RT::Handle->TransactionDepth;
+
+ MaybeEnableSQLStatementLog();
+
+ # avoid reentrancy, as suggested by masonbook
+ local *HTML::Mason::Commands::session unless $HTML::Mason::Commands::m->is_subrequest;
+
+ $HTML::Mason::Commands::m->autoflush( $HTML::Mason::Commands::m->request_comp->attr('AutoFlush') )
+ if ( $HTML::Mason::Commands::m->request_comp->attr_exists('AutoFlush') );
+
+ ValidateWebConfig();
+
+ DecodeARGS($ARGS);
+ local $HTML::Mason::Commands::DECODED_ARGS = $ARGS;
+ PreprocessTimeUpdates($ARGS);
+
+ InitializeMenu();
+ MaybeShowInstallModePage();
+
+ $HTML::Mason::Commands::m->comp( '/Elements/SetupSessionCookie', %$ARGS );
+ SendSessionCookie();
+
+ if ( _UserLoggedIn() ) {
+ # make user info up to date
+ $HTML::Mason::Commands::session{'CurrentUser'}
+ ->Load( $HTML::Mason::Commands::session{'CurrentUser'}->id );
+ }
+ else {
+ $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new();
+ }
+
+ # 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();
+
+ _ForceLogout() unless _UserLoggedIn();
+
+ # Process per-page authentication callbacks
+ $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Auth', CallbackPage => '/autohandler' );
+
+ unless ( _UserLoggedIn() ) {
+ _ForceLogout();
+
+ # 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 =~ m{^/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));
+ }
+ }
+ }
+
+ 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'} );
+
+ # Process per-page global callbacks
+ $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Default', CallbackPage => '/autohandler' );
+
+ ShowRequestedPage($ARGS);
+ LogRecordedSQLStatements(RequestData => {
+ Path => $HTML::Mason::Commands::m->request_comp->path,
+ });
+
+ # Process per-page final cleanup callbacks
+ $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Final', CallbackPage => '/autohandler' );
+
+ $HTML::Mason::Commands::m->comp( '/Elements/Footer', %$ARGS )
+ unless $HTML::Mason::Commands::r->content_type
+ =~ qr<^(text|application)/(x-)?(css|javascript)>;
+}
+
+sub _ForceLogout {
+
+ delete $HTML::Mason::Commands::session{'CurrentUser'};
+}
+
+sub _UserLoggedIn {
+ if ( $HTML::Mason::Commands::session{CurrentUser} && $HTML::Mason::Commands::session{'CurrentUser'}->id ) {
+ return 1;
+ } else {
+ return undef;
+ }
+
+}
+
+=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'}++;
+ 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
+a request to RT's Installation workflow, only if Install Mode is enabled in the configuration file.
+
+If it serves a page, it stops mason processing. Otherwise, mason just keeps running through the autohandler
+
+=cut
+
+sub MaybeShowInstallModePage {
+ return unless RT->InstallMode;
+
+ my $m = $HTML::Mason::Commands::m;
+ if ( $m->base_comp->path =~ RT->Config->Get('WebNoAuthRegex') ) {
+ $m->call_next();
+ } elsif ( $m->request_comp->path !~ m{^(/+)Install/} ) {
+ RT::Interface::Web::Redirect( RT->Config->Get('WebURL') . "Install/index.html" );
+ } else {
+ $m->call_next();
+ }
+ $m->abort();
+}
+
+=head2 MaybeShowNoAuthPage \%ARGS
+
+This function, called exclusively by RT's autohandler, dispatches
+a request to the page a user requested (but only if it matches the "noauth" regex.
+
+If it serves a page, it stops mason processing. Otherwise, mason just keeps running through the autohandler
+
+=cut
+
+sub MaybeShowNoAuthPage {
+ my $ARGS = shift;
+
+ my $m = $HTML::Mason::Commands::m;
+
+ 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.
+ $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 (_unsafe)? ) # 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;
+}
+
+sub InitializeMenu {
+ $HTML::Mason::Commands::m->notes('menu', RT::Interface::Web::Menu->new());
+ $HTML::Mason::Commands::m->notes('page-menu', RT::Interface::Web::Menu->new());
+ $HTML::Mason::Commands::m->notes('page-widgets', RT::Interface::Web::Menu->new());
+
+}
+
+
+=head2 ShowRequestedPage \%ARGS
+
+This function, called exclusively by RT's autohandler, dispatches
+a request to the page a user requested (making sure that unpriviled users
+can only see self-service pages.
+
+=cut
+
+sub ShowRequestedPage {
+ my $ARGS = shift;
+
+ my $m = $HTML::Mason::Commands::m;
+
+ # Ensure that the cookie that we send is up-to-date, in case the
+ # session-id has been modified in any way
+ SendSessionCookie();
+
+ # precache all system level rights for the current user
+ $HTML::Mason::Commands::session{CurrentUser}->PrincipalObj->HasRights( Object => RT->System );
+
+ # If the user isn't privileged, they can only see SelfService
+ unless ( $HTML::Mason::Commands::session{'CurrentUser'}->Privileged ) {
+
+ # if the user is trying to access a ticket, redirect them
+ if ( $m->request_comp->path =~ m{^(/+)Ticket/Display.html} && $ARGS->{'id'} ) {
+ RT::Interface::Web::Redirect( RT->Config->Get('WebURL') . "SelfService/Display.html?id=" . $ARGS->{'id'} );
+ }
+
+ # otherwise, drop the user at the SelfService default page
+ elsif ( $m->base_comp->path !~ RT->Config->Get('SelfServiceRegex') ) {
+ RT::Interface::Web::Redirect( RT->Config->Get('WebURL') . "SelfService/" );
+ }
+
+ # if user is in SelfService dir let him do anything
+ else {
+ $m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %$ARGS );
+ }
+ } else {
+ $m->comp( { base_comp => $m->request_comp }, $m->fetch_next, %$ARGS );
+ }
+
+}
+
+sub AttemptExternalAuth {
+ my $ARGS = shift;
+
+ return unless ( RT->Config->Get('WebExternalAuth') );
+
+ my $user = $ARGS->{user};
+ my $m = $HTML::Mason::Commands::m;
+
+ # If RT is configured for external auth, let's go through and get REMOTE_USER
+
+ # do we actually have a REMOTE_USER equivlent?
+ if ( RT::Interface::Web::WebCanonicalizeInfo() ) {
+ my $orig_user = $user;
+
+ $user = RT::Interface::Web::WebCanonicalizeInfo();
+ my $load_method = RT->Config->Get('WebExternalGecos') ? 'LoadByGecos' : 'Load';
+
+ if ( $^O eq 'MSWin32' and RT->Config->Get('WebExternalGecos') ) {
+ my $NodeName = Win32::NodeName();
+ $user =~ s/^\Q$NodeName\E\\//i;
+ }
+
+ my $next = delete $HTML::Mason::Commands::session{'NextPage'}->{$ARGS->{'next'} || ''};
+ InstantiateNewSession() unless _UserLoggedIn;
+ $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new();
+ $HTML::Mason::Commands::session{'CurrentUser'}->$load_method($user);
+
+ if ( RT->Config->Get('WebExternalAuto') and not _UserLoggedIn() ) {
+
+ # Create users on-the-fly
+ my $UserObj = RT::User->new(RT->SystemUser);
+ my ( $val, $msg ) = $UserObj->Create(
+ %{ ref RT->Config->Get('AutoCreate') ? RT->Config->Get('AutoCreate') : {} },
+ Name => $user,
+ Gecos => $user,
+ );
+
+ if ($val) {
+
+ # now get user specific information, to better create our user.
+ my $new_user_info = RT::Interface::Web::WebExternalAutoInfo($user);
+
+ # set the attributes that have been defined.
+ foreach my $attribute ( $UserObj->WritableAttributes ) {
+ $m->callback(
+ Attribute => $attribute,
+ User => $user,
+ UserInfo => $new_user_info,
+ CallbackName => 'NewUser',
+ CallbackPage => '/autohandler'
+ );
+ my $method = "Set$attribute";
+ $UserObj->$method( $new_user_info->{$attribute} ) if defined $new_user_info->{$attribute};
+ }
+ $HTML::Mason::Commands::session{'CurrentUser'}->Load($user);
+ } else {
+
+ # we failed to successfully create the user. abort abort abort.
+ delete $HTML::Mason::Commands::session{'CurrentUser'};
+
+ if (RT->Config->Get('WebFallbackToInternalAuth')) {
+ TangentForLoginWithError('Cannot create user: [_1]', $msg);
+ } else {
+ $m->abort();
+ }
+ }
+ }
+
+ if ( _UserLoggedIn() ) {
+ $m->callback( %$ARGS, CallbackName => 'ExternalAuthSuccessfulLogin', CallbackPage => '/autohandler' );
+ # It is possible that we did a redirect to the login page,
+ # if the external auth allows lack of auth through with no
+ # REMOTE_USER set, instead of forcing a "permission
+ # denied" message. Honor the $next.
+ Redirect($next) if $next;
+ # Unlike AttemptPasswordAuthentication below, we do not
+ # force a redirect to / if $next is not set -- otherwise,
+ # straight-up external auth would always redirect to /
+ # when you first hit it.
+ } else {
+ delete $HTML::Mason::Commands::session{'CurrentUser'};
+ $user = $orig_user;
+
+ unless ( RT->Config->Get('WebFallbackToInternalAuth') ) {
+ 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)
+ TangentForLoginWithError('You are not an authorized user');
+ }
+ } else {
+
+ # WebExternalAuth is set, but we don't have a REMOTE_USER. abort
+ # XXX: we must return AUTH_REQUIRED status or we fallback to
+ # internal auth here too.
+ delete $HTML::Mason::Commands::session{'CurrentUser'}
+ if defined $HTML::Mason::Commands::session{'CurrentUser'};
+ }
+}
+
+sub AttemptPasswordAuthentication {
+ my $ARGS = shift;
+ return unless defined $ARGS->{user} && defined $ARGS->{pass};
+
+ my $user_obj = RT::CurrentUser->new();
+ $user_obj->Load( $ARGS->{user} );
+
+ my $m = $HTML::Mason::Commands::m;
+
+ unless ( $user_obj->id && $user_obj->IsPassword( $ARGS->{pass} ) ) {
+ $RT::Logger->error("FAILED LOGIN for @{[$ARGS->{user}]} from $ENV{'REMOTE_ADDR'}");
+ $m->callback( %$ARGS, CallbackName => 'FailedLogin', CallbackPage => '/autohandler' );
+ 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;
+
+ $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
+
+Load or setup a session cookie for the current user.
+
+=cut
+
+sub _SessionCookieName {
+ my $cookiename = "RT_SID_" . RT->Config->Get('rtname');
+ $cookiename .= "." . $ENV{'SERVER_PORT'} if $ENV{'SERVER_PORT'};
+ return $cookiename;
+}
+
+sub LoadSessionFromCookie {
+
+ my %cookies = CGI::Cookie->fetch;
+ my $cookiename = _SessionCookieName();
+ my $SessionCookie = ( $cookies{$cookiename} ? $cookies{$cookiename}->value : undef );
+ tie %HTML::Mason::Commands::session, 'RT::Interface::Web::Session', $SessionCookie;
+ unless ( $SessionCookie && $HTML::Mason::Commands::session{'_session_id'} eq $SessionCookie ) {
+ undef $cookies{$cookiename};
+ }
+ if ( int RT->Config->Get('AutoLogoff') ) {
+ my $now = int( time / 60 );
+ my $last_update = $HTML::Mason::Commands::session{'_session_last_update'} || 0;
+
+ if ( $last_update && ( $now - $last_update - RT->Config->Get('AutoLogoff') ) > 0 ) {
+ InstantiateNewSession();
+ }
+
+ # save session on each request when AutoLogoff is turned on
+ $HTML::Mason::Commands::session{'_session_last_update'} = $now if $now != $last_update;
+ }
+}
+
+sub InstantiateNewSession {
+ tied(%HTML::Mason::Commands::session)->delete if tied(%HTML::Mason::Commands::session);
+ tie %HTML::Mason::Commands::session, 'RT::Interface::Web::Session', undef;
+ SendSessionCookie();
+}
+
+sub SendSessionCookie {
+ my $cookie = CGI::Cookie->new(
+ -name => _SessionCookieName(),
+ -value => $HTML::Mason::Commands::session{_session_id},
+ -path => RT->Config->Get('WebPath'),
+ -secure => ( RT->Config->Get('WebSecureCookies') ? 1 : 0 ),
+ -httponly => ( RT->Config->Get('WebHttpOnlyCookies') ? 1 : 0 ),