[Rt-commit] rt branch 6.0/non-blocking-sessions created. rt-5.0.3-502-gdc492ebe02

BPS Git Server git at git.bestpractical.com
Thu Apr 20 14:38:06 UTC 2023


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "rt".

The branch, 6.0/non-blocking-sessions has been created
        at  dc492ebe0286147fa6807f204d8a85585e722f3e (commit)

- Log -----------------------------------------------------------------
commit dc492ebe0286147fa6807f204d8a85585e722f3e
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Thu Apr 20 10:24:30 2023 -0400

    Test for actions message in forward

diff --git a/t/web/ticket_forward.t b/t/web/ticket_forward.t
index 23c7838053..6c2a70a123 100644
--- a/t/web/ticket_forward.t
+++ b/t/web/ticket_forward.t
@@ -43,6 +43,7 @@ diag "Forward Ticket" if $ENV{TEST_VERBOSE};
         },
         button => 'ForwardAndReturn'
     );
+    $m->content_contains('Message recorded', 'Actions message is shown on ticket');
     $m->content_contains(
         'Forwarded Ticket to Foo <rt-foo at example.com>, <rt-too at example.com>, <rt-cc at example.com>, root (Enoch Root)',
         'txn msg' );

commit 73badcf4180f41269dd09113ee24003db6dc38f4
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Thu Apr 20 10:23:58 2023 -0400

    Update session messages for ticket forward

diff --git a/share/html/Ticket/Forward.html b/share/html/Ticket/Forward.html
index 4b42e29105..260f69e433 100644
--- a/share/html/Ticket/Forward.html
+++ b/share/html/Ticket/Forward.html
@@ -159,7 +159,15 @@ if ( !$checks_failure && ($Forward || $ForwardAndReturn) ) {
     if ( $ForwardAndReturn ) {
         $session{'i'}++;
         my $key = Digest::MD5::md5_hex(rand(1024));
-        push @{ $session{"Actions"}->{$key}  ||= [] }, @results;
+        my $actions_ref = $session{"Actions"}->{$key} ||= [];
+        push @{$actions_ref}, @results;
+
+        RT::Interface::Web::Session::Set(
+            SessionRef    => \%HTML::Mason::Commands::session,
+            SessionKey    => 'Actions',
+            SessionSubKey => $key,
+            SessionValue  => $actions_ref,
+        );
         RT::Interface::Web::Redirect( RT->Config->Get('WebURL') ."Ticket/Display.html?id=". $id."&results=".$key);
     }
 }

commit 5c3e0b6a4d5f8544032b3be8359bb2e671f21458
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Thu Apr 20 09:50:46 2023 -0400

    Update new CF scrubbing code to non-blocking sessions
    
    The key for Actions is an empty string, so the session
    Set method needs to check for defined rather than
    just a truthy if check.

diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index 827ac1b6c7..7b0fcfc9dd 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -3820,8 +3820,15 @@ sub _NormalizeObjectCustomFieldValue {
             my $new_value
                 = ScrubHTML( Content => $values[0], Permissive => $args{CustomField}->_ContentIsPermissive );
             if ( $values[0] ne $new_value ) {
-                push @{ $session{"Actions"}->{''} }, $msg;
-                $HTML::Mason::Commands::session{'i'}++;
+                my $actions_ref = $session{"Actions"}->{''} ||= [];
+                push @{$actions_ref}, $msg;
+
+                RT::Interface::Web::Session::Set(
+                    SessionRef    => \%HTML::Mason::Commands::session,
+                    SessionKey    => 'Actions',
+                    SessionSubKey => '',
+                    SessionValue  => $actions_ref,
+                );
                 $values[0] = $new_value;
             }
         }
diff --git a/lib/RT/Interface/Web/Session.pm b/lib/RT/Interface/Web/Session.pm
index 2ec6aa066a..db4e7a113d 100644
--- a/lib/RT/Interface/Web/Session.pm
+++ b/lib/RT/Interface/Web/Session.pm
@@ -397,10 +397,10 @@ sub Set {
     }
 
     # Set the value, which will automagically set it in the back-end session storage
-    if ( $args{'SessionSubSubKey'} ) {
+    if ( defined $args{'SessionSubSubKey'} ) {
         $target->{ $args{'SessionKey'} }{ $args{'SessionSubKey'} }{ $args{'SessionSubSubKey'} } = $args{'SessionValue'};
     }
-    elsif ( $args{'SessionSubKey'} ) {
+    elsif ( defined $args{'SessionSubKey'} ) {
         $target->{ $args{'SessionKey'} }{ $args{'SessionSubKey'} } = $args{'SessionValue'};
     }
     else {

commit 0dab7ce9fcdbbda34638a7df16860fc52baa4996
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Thu Apr 20 09:26:17 2023 -0400

    Make the RT session non-blocking in a request
    
    Isolate the tied session interaction with Apache::Session
    from the full RT web request processing to avoid blocking
    requests on the session. This allows multiple requests
    to access the session at once. This should only be done
    where most requests are reads. If multiple requests
    write to the session, this will cause unpredictable
    results.

diff --git a/lib/RT/Authen/ExternalAuth.pm b/lib/RT/Authen/ExternalAuth.pm
index de21f84909..7a8358157c 100644
--- a/lib/RT/Authen/ExternalAuth.pm
+++ b/lib/RT/Authen/ExternalAuth.pm
@@ -446,6 +446,12 @@ sub DoAuth {
             $session->{'CurrentUser'}->Load($UserObj->Id);
         }
 
+        RT::Interface::Web::Session::Set(
+            SessionRef   => \%HTML::Mason::Commands::session,
+            SessionKey   => 'CurrentUser',
+            SessionValue => $session->{'CurrentUser'},
+        );
+
         ####################################################################
         ########## Authentication ##########################################
         ####################################################################
@@ -476,11 +482,21 @@ sub DoAuth {
     # get a full, valid user from an authoritative external source.
     unless ($session->{'CurrentUser'} && $session->{'CurrentUser'}->Id) {
         $session->{'CurrentUser'} = RT::CurrentUser->new;
+        RT::Interface::Web::Session::Set(
+            SessionRef   => \%HTML::Mason::Commands::session,
+            SessionKey   => 'CurrentUser',
+            SessionValue => $session->{'CurrentUser'},
+        );
         return (0, "No User");
     }
 
     unless($success) {
         $session->{'CurrentUser'} = RT::CurrentUser->new;
+        RT::Interface::Web::Session::Set(
+            SessionRef   => \%HTML::Mason::Commands::session,
+            SessionKey   => 'CurrentUser',
+            SessionValue => $session->{'CurrentUser'},
+        );
         return (0, "Password Invalid");
     }
 
@@ -516,6 +532,11 @@ sub DoAuth {
         # if the user is disabled, kick them out. Now!
         if ($session->{'CurrentUser'}->UserObj->Disabled) {
             $session->{'CurrentUser'} = RT::CurrentUser->new;
+            RT::Interface::Web::Session::Set(
+                SessionRef   => \%HTML::Mason::Commands::session,
+                SessionKey   => 'CurrentUser',
+                SessionValue => $session->{'CurrentUser'},
+            );
             return (0, "User account disabled, login denied");
         }
     }
@@ -535,9 +556,19 @@ sub DoAuth {
             my $cu = $session->{CurrentUser};
             RT::Interface::Web::InstantiateNewSession();
             $session->{CurrentUser} = $cu;
+            RT::Interface::Web::Session::Set(
+                SessionRef   => \%HTML::Mason::Commands::session,
+                SessionKey   => 'CurrentUser',
+                SessionValue => $session->{'CurrentUser'},
+            );
     } else {
             # Make SURE the session is purged to an empty user.
             $session->{'CurrentUser'} = RT::CurrentUser->new;
+            RT::Interface::Web::Session::Set(
+                SessionRef   => \%HTML::Mason::Commands::session,
+                SessionKey   => 'CurrentUser',
+                SessionValue => $session->{'CurrentUser'},
+            );
             return (0, "Failed to authenticate externally");
             # This will cause autohandler to request IsPassword
             # which will in turn call IsExternalPassword
diff --git a/lib/RT/Dashboard/Mailer.pm b/lib/RT/Dashboard/Mailer.pm
index 1812616963..d6730feb0c 100644
--- a/lib/RT/Dashboard/Mailer.pm
+++ b/lib/RT/Dashboard/Mailer.pm
@@ -397,6 +397,7 @@ SUMMARY
     local $HTML::Mason::Commands::session{CurrentUser} = $currentuser;
     local $HTML::Mason::Commands::session{ContextUser} = $context_user;
     local $HTML::Mason::Commands::session{WebDefaultStylesheet} = 'elevator-light';
+    local $HTML::Mason::Commands::session{_session_id}; # Make sure to not touch sessions table
     local $HTML::Mason::Commands::r = RT::Dashboard::FakeRequest->new;
 
     my $HasResults = undef;
diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index d5745fcad2..827ac1b6c7 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -355,6 +355,13 @@ sub HandleRequest {
         $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new();
     }
 
+    # Write changes back to persistent session
+    RT::Interface::Web::Session::Set(
+        SessionRef   => \%HTML::Mason::Commands::session,
+        SessionKey   => 'CurrentUser',
+        SessionValue => $HTML::Mason::Commands::session{'CurrentUser'},
+    );
+
     # attempt external auth
     $HTML::Mason::Commands::m->comp( '/Elements/DoAuth', %$ARGS )
         if @{ RT->Config->Get( 'ExternalAuthPriority' ) || [] };
@@ -380,7 +387,11 @@ sub HandleRequest {
     $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Auth', CallbackPage => '/autohandler' );
 
     if ( $ARGS->{'NotMobile'} ) {
-        $HTML::Mason::Commands::session{'NotMobile'} = 1;
+        RT::Interface::Web::Session::Set(
+            SessionRef   => \%HTML::Mason::Commands::session,
+            SessionKey   => 'NotMobile',
+            SessionValue => 1,
+        );
     }
 
     unless ( _UserLoggedIn() ) {
@@ -419,8 +430,13 @@ 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'} );
+    if ( $ARGS->{'HomeRefreshInterval'} ) {
+        RT::Interface::Web::Session::Set(
+            SessionRef   => \%HTML::Mason::Commands::session,
+            SessionKey   => 'home_refresh_interval',
+            SessionValue => $ARGS->{'HomeRefreshInterval'},
+        );
+    }
 
     # Process per-page global callbacks
     $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Default', CallbackPage => '/autohandler' );
@@ -438,7 +454,10 @@ sub HandleRequest {
 
 sub _ForceLogout {
 
-    delete $HTML::Mason::Commands::session{'CurrentUser'};
+    RT::Interface::Web::Session::Delete(
+        SessionRef => \%HTML::Mason::Commands::session,
+        SessionKey => 'CurrentUser',
+    );
 }
 
 sub _UserLoggedIn {
@@ -459,8 +478,15 @@ Pushes a login error into the Actions session store and returns the hash key.
 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'}++;
+
+    my @actions = @{ $HTML::Mason::Commands::session{"Actions"}->{$key} ||= [] };
+    push @actions, $new;
+    RT::Interface::Web::Session::Set(
+        SessionRef   => \%HTML::Mason::Commands::session,
+        SessionKey   => 'Actions',
+        SessionValue => \@actions,
+    );
+
     return $key;
 }
 
@@ -496,8 +522,13 @@ sub SetNextPage {
         }
     }
 
-    $HTML::Mason::Commands::session{'NextPage'}->{$hash} = $page;
-    $HTML::Mason::Commands::session{'i'}++;
+    RT::Interface::Web::Session::Set(
+        SessionRef    => \%HTML::Mason::Commands::session,
+        SessionKey    => 'NextPage',
+        SessionSubKey => $hash,
+        SessionValue  => $page,
+    );
+
     return $hash;
 }
 
@@ -509,6 +540,11 @@ Returns the stashed next page hashref for the given hash.
 
 sub FetchNextPage {
     my $hash = shift || "";
+    RT::Interface::Web::Session::Load(
+        SessionRef => \%HTML::Mason::Commands::session,
+        SessionId  => $HTML::Mason::Commands::session{'_session_id'},
+    );
+
     return $HTML::Mason::Commands::session{'NextPage'}->{$hash};
 }
 
@@ -520,7 +556,13 @@ Removes the stashed next page for the given hash and returns it.
 
 sub RemoveNextPage {
     my $hash = shift || "";
-    return delete $HTML::Mason::Commands::session{'NextPage'}->{$hash};
+    my $return_hash = $HTML::Mason::Commands::session{'NextPage'}->{$hash};
+    RT::Interface::Web::Session::Delete(
+        SessionRef    => \%HTML::Mason::Commands::session,
+        SessionKey    => 'NextPage',
+        SessionSubKey => $hash,
+    );
+    return $return_hash;
 }
 
 =head2 TangentForLogin ARGSRef [HASH]
@@ -781,6 +823,12 @@ sub AttemptExternalAuth {
         $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new();
         $HTML::Mason::Commands::session{'CurrentUser'}->$load_method($user);
 
+        RT::Interface::Web::Session::Set(
+            SessionRef   => \%HTML::Mason::Commands::session,
+            SessionKey   => 'CurrentUser',
+            SessionValue => $HTML::Mason::Commands::session{'CurrentUser'},
+        );
+
         if ( RT->Config->Get('WebRemoteUserAutocreate') and not _UserLoggedIn() ) {
 
             # Create users on-the-fly
@@ -809,6 +857,11 @@ sub AttemptExternalAuth {
                     $UserObj->$method( $new_user_info->{$attribute} ) if defined $new_user_info->{$attribute};
                 }
                 $HTML::Mason::Commands::session{'CurrentUser'}->Load($user);
+                RT::Interface::Web::Session::Set(
+                    SessionRef   => \%HTML::Mason::Commands::session,
+                    SessionKey   => 'CurrentUser',
+                    SessionValue => $HTML::Mason::Commands::session{'CurrentUser'},
+                );
             } else {
                 RT->Logger->error("Couldn't auto-create user '$user' when attempting WebRemoteUser: $msg");
                 AbortExternalAuth( Error => "UserAutocreateDefaultsOnLogin" );
@@ -817,7 +870,13 @@ sub AttemptExternalAuth {
 
         if ( _UserLoggedIn() ) {
             RT->Logger->info("Session created from REMOTE_USER for user $user from " . RequestENV('REMOTE_ADDR'));
-            $HTML::Mason::Commands::session{'WebExternallyAuthed'} = 1;
+
+            RT::Interface::Web::Session::Set(
+                SessionRef   => \%HTML::Mason::Commands::session,
+                SessionKey   => 'WebExternallyAuthed',
+                SessionValue => 1,
+            );
+
             $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
@@ -900,7 +959,12 @@ sub AttemptPasswordAuthentication {
            $next = $next->{'url'} if ref $next;
 
         InstantiateNewSession();
-        $HTML::Mason::Commands::session{'CurrentUser'} = $user_obj;
+
+        RT::Interface::Web::Session::Set(
+            SessionRef   => \%HTML::Mason::Commands::session,
+            SessionKey   => 'CurrentUser',
+            SessionValue => $user_obj,
+        );
 
         $m->callback( %$ARGS, CallbackName => 'SuccessfulLogin', CallbackPage => '/autohandler', RedirectTo => \$next );
 
@@ -935,7 +999,11 @@ sub AttemptTokenAuthentication {
             $next = $next->{'url'} if ref $next;
 
             RT::Interface::Web::InstantiateNewSession();
-            $HTML::Mason::Commands::session{'CurrentUser'} = $user_obj;
+            RT::Interface::Web::Session::Set(
+                SessionRef   => \%HTML::Mason::Commands::session,
+                SessionKey   => 'CurrentUser',
+                SessionValue => $user_obj,
+            );
 
             # 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.
@@ -968,7 +1036,12 @@ sub LoadSessionFromCookie {
     my %cookies       = CGI::Cookie->parse(RequestENV('HTTP_COOKIE'));
     my $cookiename    = _SessionCookieName();
     my $SessionCookie = ( $cookies{$cookiename} ? $cookies{$cookiename}->value : undef );
-    tie %HTML::Mason::Commands::session, 'RT::Interface::Web::Session', $SessionCookie;
+
+    RT::Interface::Web::Session::Load(
+        SessionRef => \%HTML::Mason::Commands::session,
+        SessionId  => $SessionCookie,
+    );
+
     unless ( $SessionCookie && $HTML::Mason::Commands::session{'_session_id'} eq $SessionCookie ) {
         InstantiateNewSession();
     }
@@ -981,13 +1054,25 @@ sub LoadSessionFromCookie {
         }
 
         # save session on each request when AutoLogoff is turned on
-        $HTML::Mason::Commands::session{'_session_last_update'} = $now if $now != $last_update;
+        if ( $now != $last_update ) {
+            RT::Interface::Web::Session::Set(
+                SessionRef   => \%HTML::Mason::Commands::session,
+                SessionKey   => '_session_last_update',
+                SessionValue => $now,
+            );
+        }
     }
 }
 
 sub InstantiateNewSession {
-    tied(%HTML::Mason::Commands::session)->delete if tied(%HTML::Mason::Commands::session);
-    tie %HTML::Mason::Commands::session, 'RT::Interface::Web::Session', undef;
+    # Starting a new session, so clear out any existing one
+    RT::Interface::Web::Session::Delete( SessionRef   => \%HTML::Mason::Commands::session );
+
+    RT::Interface::Web::Session::Load(
+        SessionRef => \%HTML::Mason::Commands::session,
+        SessionId  => undef,
+    );
+
     SendSessionCookie();
 }
 
@@ -1034,10 +1119,9 @@ a cached DBI statement handle twice at the same time.
 
 sub Redirect {
     my $redir_to = shift;
-    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) {
@@ -1676,8 +1760,14 @@ sub IsPossibleCSRF {
     # 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'};
+
+    unless ( defined $HTML::Mason::Commands::session{'REST'} ) {
+        RT::Interface::Web::Session::Set(
+            SessionRef   => \%HTML::Mason::Commands::session,
+            SessionKey   => 'REST',
+            SessionValue => scalar( $path =~ m{^/+REST/\d+\.\d+(/|$)} ),
+        );
+    }
 
     if ($HTML::Mason::Commands::session{'REST'}) {
         return 0 if $path =~ m{^/+REST/\d+\.\d+(/|$)};
@@ -1743,8 +1833,14 @@ sub ExpandCSRFToken {
     if ($data->{attach}) {
         my $filename = $data->{attach}{filename};
         my $mime     = $data->{attach}{mime};
-        $HTML::Mason::Commands::session{'Attachments'}{$ARGS->{'Token'}||''}{$filename}
-            = $mime;
+
+        RT::Interface::Web::Session::Set(
+            SessionRef       => \%HTML::Mason::Commands::session,
+            SessionKey       => 'Attachments',
+            SessionSubKey    => $ARGS->{'Token'}||'',
+            SessionSubSubKey => $filename,
+            SessionValue     => $mime,
+        );
     }
 
     return 1;
@@ -1773,8 +1869,13 @@ sub StoreRequestToken {
         };
     }
 
-    $HTML::Mason::Commands::session{'CSRF'}->{$token} = $data;
-    $HTML::Mason::Commands::session{'i'}++;
+    RT::Interface::Web::Session::Set(
+        SessionRef    => \%HTML::Mason::Commands::session,
+        SessionKey    => 'CSRF',
+        SessionSubKey => $token,
+        SessionValue  => $data,
+    );
+
     return $token;
 }
 
@@ -2284,8 +2385,19 @@ sub MaybeRedirectForResults {
 
     if ( $has_actions ) {
         my $key = Digest::MD5::md5_hex( rand(1024) );
-        push @{ $session{"Actions"}{ $key } ||= [] }, @{ $args{'Actions'} };
-        $session{'i'}++;
+        my $actions_ref = [];
+        if ( $session{"Actions"}{ $key } ) {
+            $actions_ref = $session{"Actions"}{ $key };
+        }
+        push @{$actions_ref}, @{ $args{'Actions'} };
+
+        RT::Interface::Web::Session::Set(
+            SessionRef    => \%HTML::Mason::Commands::session,
+            SessionKey    => 'Actions',
+            SessionSubKey => $key,
+            SessionValue  => $actions_ref,
+        );
+
         $arguments{'results'} = $key;
     }
 
@@ -2402,10 +2514,13 @@ sub CreateTicket {
     if ( my $tmp = $session{'Attachments'}{ $ARGS{'Token'} || '' } ) {
         push @attachments, grep $_, map $tmp->{$_}, sort keys %$tmp;
 
-        delete $session{'Attachments'}{ $ARGS{'Token'} || '' }
-            unless $ARGS{'KeepAttachments'} or $Ticket->{DryRun};
-        $session{'Attachments'} = $session{'Attachments'}
-            if @attachments;
+        unless ( $ARGS{'KeepAttachments'} or $Ticket->{DryRun} ) {
+            RT::Interface::Web::Session::Delete(
+                SessionRef    => \%HTML::Mason::Commands::session,
+                SessionKey    => 'Attachments',
+                SessionSubKey => $ARGS{'Token'} || '',
+            );
+        }
     }
     if ( $ARGS{'Attachments'} ) {
         push @attachments, grep $_, map $ARGS{Attachments}->{$_}, sort keys %{ $ARGS{'Attachments'} };
@@ -2538,11 +2653,13 @@ sub ProcessUpdateMessage {
     if ( my $tmp = $session{'Attachments'}{ $args{'ARGSRef'}{'Token'} || '' } ) {
         push @attachments, grep $_, map $tmp->{$_}, sort keys %$tmp;
 
-        delete $session{'Attachments'}{ $args{'ARGSRef'}{'Token'} || '' }
-            unless $args{'KeepAttachments'}
-            or ($args{TicketObj} and $args{TicketObj}{DryRun});
-        $session{'Attachments'} = $session{'Attachments'}
-            if @attachments;
+        unless ( $args{'KeepAttachments'} or ( $args{TicketObj} and $args{TicketObj}{DryRun} ) ) {
+            RT::Interface::Web::Session::Delete(
+                SessionRef    => \%HTML::Mason::Commands::session,
+                SessionKey    => 'Attachments',
+                SessionSubKey => $args{'ARGSRef'}{'Token'} || '',
+            );
+        }
     }
     if ( $args{ARGSRef}{'UpdateAttachments'} ) {
         push @attachments, grep $_, map $args{ARGSRef}->{UpdateAttachments}{$_},
@@ -2710,14 +2827,16 @@ sub ProcessAttachments {
     my $token = $args{'ARGSRef'}{'Token'}
         ||= $args{'Token'} ||= Digest::MD5::md5_hex( rand(1024) );
 
-    my $update_session = 0;
-
     # deal with deleting uploaded attachments
     if ( my $del = $args{'ARGSRef'}{'DeleteAttach'} ) {
-        delete $session{'Attachments'}{ $token }{ $_ }
-            foreach ref $del? @$del : ($del);
-
-        $update_session = 1;
+        foreach my $delete ( ref $del ? @$del : ($del) ) {
+            RT::Interface::Web::Session::Delete(
+                SessionRef       => \%HTML::Mason::Commands::session,
+                SessionKey       => 'Attachments',
+                SessionSubKey    => $token,
+                SessionSubSubKey => $delete,
+            );
+        }
     }
 
     # store the uploaded attachment in session
@@ -2749,11 +2868,15 @@ sub ProcessAttachments {
             }
         }
 
-        $session{'Attachments'}{ $token }{ $file_path } = $attachment;
-
-        $update_session = 1;
+        RT::Interface::Web::Session::Set(
+            SessionRef       => \%HTML::Mason::Commands::session,
+            SessionKey       => 'Attachments',
+            SessionSubKey    => $token,
+            SessionSubSubKey => $file_path,
+            SessionValue     => $attachment,
+        );
     }
-    $session{'Attachments'} = $session{'Attachments'} if $update_session;
+
     return 1;
 }
 
@@ -4304,7 +4427,13 @@ sub ProcessQuickCreate {
             );
         }
 
-        $session{QuickCreate} = \%ARGS unless $created;
+        unless ( $created ) {
+            RT::Interface::Web::Session::Set(
+                SessionRef   => \%HTML::Mason::Commands::session,
+                SessionKey   => 'QuickCreate',
+                SessionValue => \%ARGS,
+            );
+        }
 
         MaybeRedirectForResults(
             Actions   => \@results,
@@ -4725,16 +4854,25 @@ sub SetObjectSessionCache {
         CheckRight => $CheckRight, ShowAll => $ShowAll );
 
     if ( defined $session{$cache_key} && !$session{$cache_key}{id} ) {
-        delete $session{$cache_key};
+        RT::Interface::Web::Session::Delete(
+            SessionRef => \%HTML::Mason::Commands::session,
+            SessionKey => $cache_key,
+        );
     }
 
     if ( defined $session{$cache_key}
          && ref $session{$cache_key} eq 'ARRAY') {
-        delete $session{$cache_key};
+         RT::Interface::Web::Session::Delete(
+             SessionRef => \%HTML::Mason::Commands::session,
+             SessionKey => $cache_key,
+         );
     }
     if ( defined $session{$cache_key} && defined $CacheNeedsUpdate &&
         $session{$cache_key}{lastupdated} <= $CacheNeedsUpdate ) {
-        delete $session{$cache_key};
+        RT::Interface::Web::Session::Delete(
+            SessionRef => \%HTML::Mason::Commands::session,
+            SessionKey => $cache_key,
+        );
     }
 
     if ( not defined $session{$cache_key} ) {
@@ -4745,24 +4883,36 @@ sub SetObjectSessionCache {
             CallbackPage => '/Elements/Quicksearch',
             ARGSRef => \%args, Collection => $collection, ObjectType => $ObjectType );
 
-        $session{$cache_key}{id} = {};
+        RT::Interface::Web::Session::Delete(
+            SessionRef => \%HTML::Mason::Commands::session,
+            SessionKey => $cache_key,
+        );
 
+        my %ids;
         while (my $object = $collection->Next) {
             if ($ShowAll
                 or not $CheckRight
                 or $session{CurrentUser}->HasRight( Object => $object, Right => $CheckRight ))
             {
                 next if $args{'Exclude'} and exists $args{'Exclude'}->{$object->Name};
-                push @{$session{$cache_key}{objects}}, {
+                push @{$ids{objects}}, {
                     Id          => $object->Id,
                     Name        => $object->Name,
                     Description => $object->_Accessible("Description" => "read") ? $object->Description : undef,
                     Lifecycle   => $object->_Accessible("Lifecycle" => "read") ? $object->Lifecycle : undef,
                 };
-                $session{$cache_key}{id}{ $object->id } = 1;
+                $ids{id}{ $object->id } = 1;
             }
         }
-        $session{$cache_key}{lastupdated} = time();
+
+        $ids{'lastupdated'} = time();
+
+        RT::Interface::Web::Session::Set(
+            SessionRef   => \%HTML::Mason::Commands::session,
+            SessionKey   => $cache_key,
+            SessionValue => \%ids,
+        );
+
     }
 
     return $cache_key;
diff --git a/lib/RT/Interface/Web/Session.pm b/lib/RT/Interface/Web/Session.pm
index 34fcd045d4..2ec6aa066a 100644
--- a/lib/RT/Interface/Web/Session.pm
+++ b/lib/RT/Interface/Web/Session.pm
@@ -51,6 +51,7 @@ use warnings;
 use strict;
 
 use RT::CurrentUser;
+use Clone;
 
 =head1 NAME
 
@@ -316,6 +317,189 @@ sub ClearByUser {
     $self->ClearOrphanLockFiles if $deleted;
 }
 
+=head3 Load
+
+Load a session or create a new one.
+
+Accepts: SessionRef => \%session, SessionId
+
+SessionRef is a reference to a hash which will be loaded with
+session data.
+
+SessionId is the id of an existing session. If set to undef or
+omitted, an empty new session will be created with a session id
+set with the key '_session_id'.
+
+=cut
+
+sub Load {
+    my %args = (
+        SessionRef => undef,
+        SessionId  => undef,
+        @_
+    );
+
+    my %local_session;
+    tie %local_session, 'RT::Interface::Web::Session', $args{'SessionId'};
+
+    # Use { %local_session } instead of \%local_session to not clone the tie part.
+    %{ $args{'SessionRef'} } = %{ Clone::clone( {%local_session} ) };
+
+    untie %local_session;
+
+    return 1;
+}
+
+=head3 Set
+
+Set a value in the session.
+
+Accepts: SessionRef => \%session, SessionKey, SessionSubKey,
+SessionSubSubKey, SessionValue
+
+SessionRef is a reference to a hash for an existing session.
+It is expected to have a key '_session_id' with the id of the
+current session. The referenced hash will be also be updated
+with the new value.
+
+SessionKey and the SubKey parameters indicate where in the hash
+to set the value. The multiple subkey arguments handle multiple
+hash levels from the previous direct hash implementation.
+
+SessionValue is the value to set in the indicated key.
+
+If _session_id is not set, it simply updates SessionRef.
+
+=cut
+
+sub Set {
+    my %args = (
+        SessionRef       => undef,
+        SessionKey       => undef,
+        SessionSubKey    => undef,
+        SessionSubSubKey => undef,
+        SessionValue     => undef,
+        @_
+    );
+
+    my $session_id = $args{'SessionRef'}->{'_session_id'};
+
+    my %local_session;
+    my $target;
+
+    if ($session_id) {
+        tie %local_session, 'RT::Interface::Web::Session', $session_id;
+        $target = \%local_session;
+    }
+    else {
+        # No session_id means not tied, in which case SessionRef is a plain hashref.
+        $target = $args{'SessionRef'};
+    }
+
+    # Set the value, which will automagically set it in the back-end session storage
+    if ( $args{'SessionSubSubKey'} ) {
+        $target->{ $args{'SessionKey'} }{ $args{'SessionSubKey'} }{ $args{'SessionSubSubKey'} } = $args{'SessionValue'};
+    }
+    elsif ( $args{'SessionSubKey'} ) {
+        $target->{ $args{'SessionKey'} }{ $args{'SessionSubKey'} } = $args{'SessionValue'};
+    }
+    else {
+        $target->{ $args{'SessionKey'} } = $args{'SessionValue'};
+    }
+
+    if ( tied %local_session ) {
+
+        # Clone it back so we update the copy of the session with the latest values
+        # Use { %local_session } instead of \%local_session to not clone the tie part.
+        %{ $args{'SessionRef'} } = %{ Clone::clone( {%local_session} ) };
+
+        # Apache::Session doesn't sync changes to subkeys, so force a sync
+        # with a change at the top level.
+        $local_session{i}++;
+
+        untie %local_session;
+    }
+
+    return 1;
+}
+
+=head3 Delete
+
+Delete a key from the session.
+
+Accepts: SessionRef => \%session, SessionKey, SessionSubKey,
+SessionSubSubKey
+
+SessionRef is a reference to a hash for an existing session.
+It is expected to have a key '_session_id' with the id of the
+current session. The referenced hash will be also be updated
+with the new value.
+
+SessionKey and the SubKey parameters indicate where in the hash
+to delete the key. The multiple subkey arguments handle multiple
+hash levels from the previous direct hash implementation.
+
+If _session_id is not set, it simply deletes from SessionRef.
+
+=cut
+
+sub Delete {
+    my %args = (
+        SessionRef       => undef,
+        SessionKey       => undef,
+        SessionSubKey    => undef,
+        SessionSubSubKey => undef,
+        @_
+    );
+
+    my $session_id = $args{'SessionRef'}->{'_session_id'};
+    my %local_session;
+
+    my $target;
+
+    if ($session_id) {
+        tie %local_session, 'RT::Interface::Web::Session', $session_id;
+        $target = \%local_session;
+    }
+    else {
+        # No session_id means not tied, in which case SessionRef is a plain hashref.
+        $target = $args{'SessionRef'};
+    }
+
+    if ( $args{'SessionKey'} ) {
+
+        # Delete requested item from the session
+        if ( defined $args{'SessionSubSubKey'} ) {
+            delete $target->{ $args{'SessionKey'} }{ $args{'SessionSubKey'} }{ $args{'SessionSubSubKey'} };
+        }
+        elsif ( defined $args{'SessionSubKey'} ) {
+            delete $target->{ $args{'SessionKey'} }{ $args{'SessionSubKey'} };
+        }
+        else {
+            delete $target->{ $args{'SessionKey'} };
+        }
+
+        if ( tied %local_session ) {
+
+            # Apache::Session doesn't sync changes to subkeys, so force a sync
+            # with a change at the top level.
+            $local_session{i}++;
+
+            # Use { %local_session } instead of \%local_session to not clone the tie part.
+            %{ $args{'SessionRef'} } = %{ Clone::clone( {%local_session} ) };
+        }
+    }
+    else {
+        # No key provided, delete the whole session
+        tied(%local_session)->delete if tied %local_session;
+        %{ $args{'SessionRef'} } = ();
+    }
+
+    untie %local_session if tied %local_session;
+
+    return 1;
+}
+
 sub TIEHASH {
     my $self = shift;
     my $id = shift;
diff --git a/share/html/Admin/Global/DashboardsInMenu.html b/share/html/Admin/Global/DashboardsInMenu.html
index f72d8de206..3181f1851f 100644
--- a/share/html/Admin/Global/DashboardsInMenu.html
+++ b/share/html/Admin/Global/DashboardsInMenu.html
@@ -132,7 +132,10 @@ if ($ARGS{UpdateSearches}) {
                 );
             }
             push @actions, $ok ? loc('Global dashboards in menu saved.') : $msg;
-            delete $session{'dashboards_in_menu'};
+            RT::Interface::Web::Session::Delete(
+                SessionRef => \%session,
+                SessionKey => 'dashboards_in_menu',
+            );
         }
         else {
           my $report_names = ref $ARGS{'report'} eq 'ARRAY' ? $ARGS{'report'} : [$ARGS{'report'}];
@@ -155,7 +158,10 @@ if ($ARGS{UpdateSearches}) {
               );
           }
           push @actions, $ok ? loc('Preferences saved for reports in menu.') : $msg;
-          delete $session{'reports_in_menu'};
+          RT::Interface::Web::Session::Delete(
+              SessionRef => \%session,
+              SessionKey => 'reports_in_menu',
+          );
         }
     }
 }
diff --git a/share/html/Admin/Users/DashboardsInMenu.html b/share/html/Admin/Users/DashboardsInMenu.html
index fa4f0e248e..15fb7262f3 100644
--- a/share/html/Admin/Users/DashboardsInMenu.html
+++ b/share/html/Admin/Users/DashboardsInMenu.html
@@ -150,7 +150,10 @@ if ($ARGS{UpdateSearches}) {
 
       my ( $ok, $msg ) = $UserObj->SetPreferences( $ARGS{'dashboard_id'}, { 'dashboards' => \@dashboard_ids } );
       push @actions, $ok ? loc('Preferences saved for dashboards in menu.') : $msg;
-      delete $session{'dashboards_in_menu'};
+      RT::Interface::Web::Session::Delete(
+          SessionRef => \%session,
+          SessionKey => 'dashboards_in_menu',
+      );
   }
   else {
     my $report_names = ref $ARGS{'report'} eq 'ARRAY' ? $ARGS{'report'} : [$ARGS{'report'}];
@@ -163,7 +166,10 @@ if ($ARGS{UpdateSearches}) {
 
     my ( $ok, $msg ) = $UserObj->SetPreferences( $ARGS{'dashboard_id'}, \@ret );
     push @actions, $ok ? loc('Preferences saved for reports in menu.') : $msg;
-    delete $session{'reports_in_menu'};
+    RT::Interface::Web::Session::Delete(
+        SessionRef => \%session,
+        SessionKey => 'reports_in_menu',
+    );
   }
 }
 
diff --git a/share/html/Asset/Create.html b/share/html/Asset/Create.html
index 61170a22e4..c9e154e50b 100644
--- a/share/html/Asset/Create.html
+++ b/share/html/Asset/Create.html
@@ -114,7 +114,11 @@ Abort(loc("You don't have permission to create assets in catalog [_1].",
     unless $catalog->CurrentUserHasRight("CreateAsset");
 
 # Update the current default with the latest selection
-$session{'DefaultCatalog'} = $catalog->Id;
+RT::Interface::Web::Session::Set(
+    SessionRef   => \%session,
+    SessionKey   => 'DefaultCatalog',
+    SessionValue => $catalog->Id,
+);
 
 my @results;
 
diff --git a/share/html/Asset/Elements/SelectCatalog b/share/html/Asset/Elements/SelectCatalog
index 0f003909c6..ad879f58ac 100644
--- a/share/html/Asset/Elements/SelectCatalog
+++ b/share/html/Asset/Elements/SelectCatalog
@@ -65,7 +65,11 @@ $AutoSubmit     => 0
 <%init>
 my $catalog_obj = LoadDefaultCatalog($Default || '');
 if ( $UpdateSession && $catalog_obj->Id ){
-    $session{'DefaultCatalog'} = $catalog_obj->Id;
+    RT::Interface::Web::Session::Set(
+        SessionRef   => \%session,
+        SessionKey   => 'DefaultCatalog',
+        SessionValue => $catalog_obj->Id,
+    );
     $Default = $catalog_obj->Id;
 }
 $ARGS{OnChange} = "jQuery(this).closest('form').find('input[name=CatalogChanged]').val(1);";
diff --git a/share/html/Dashboards/Render.html b/share/html/Dashboards/Render.html
index d73beb8ff1..63b16dc8ca 100644
--- a/share/html/Dashboards/Render.html
+++ b/share/html/Dashboards/Render.html
@@ -151,7 +151,14 @@ for my $sub ($session{'CurrentUser'}->UserObj->Attributes->Named('Subscription')
     last;
 }
 
-$session{ContextUser} ||= $session{CurrentUser};
+if ( !$session{ContextUser} ) {
+   RT::Interface::Web::Session::Set(
+       SessionRef   => \%session,
+       SessionKey   => 'ContextUser',
+       SessionValue => $session{CurrentUser},
+   );
+}
+
 # otherwise honor their search preferences.. otherwise default search result rows and fall back to 50
 # $rows == 0 means unlimited, which we don't want to ignore from above
 unless (defined($rows)) {
diff --git a/share/html/Elements/ListActions b/share/html/Elements/ListActions
index 0fd05fda49..65abfb1805 100644
--- a/share/html/Elements/ListActions
+++ b/share/html/Elements/ListActions
@@ -61,20 +61,31 @@
 
 # backward compatibility, don't use array in new code, but use keyed hash
 if ( ref( $session{'Actions'} ) eq 'ARRAY' ) {
-    unshift @actions, @{ delete $session{'Actions'} };
-    $session{'i'}++;
+    unshift @actions, @{ $session{'Actions'} };
+    RT::Interface::Web::Session::Delete(
+        SessionRef => \%session,
+        SessionKey => 'Actions',
+    );
 }
 
 if ( ref( $session{'Actions'}{''} ) eq 'ARRAY' ) {
-    unshift @actions, @{ delete $session{'Actions'}{''} };
-    $session{'i'}++;
+    unshift @actions, @{ $session{'Actions'}{''} };
+    RT::Interface::Web::Session::Delete(
+        SessionRef    => \%session,
+        SessionKey    => 'Actions',
+        SessionSubKey => '',
+    );
 }
 
 my $actions_pointer = $DECODED_ARGS->{'results'};
 
 if ($actions_pointer &&  ref( $session{'Actions'}->{$actions_pointer} ) eq 'ARRAY' ) {
-    unshift @actions, @{ delete $session{'Actions'}->{$actions_pointer} };
-    $session{'i'}++;
+    unshift @actions, @{ $session{'Actions'}->{$actions_pointer} };
+    RT::Interface::Web::Session::Delete(
+        SessionRef    => \%session,
+        SessionKey    => 'Actions',
+        SessionSubKey => $actions_pointer,
+    );
 }
 
 # XXX: run callbacks per row really crazy idea
diff --git a/share/html/Elements/QuickCreate b/share/html/Elements/QuickCreate
index 4def33febb..6453bfb8f1 100644
--- a/share/html/Elements/QuickCreate
+++ b/share/html/Elements/QuickCreate
@@ -81,5 +81,9 @@
 </div>
 
 <%INIT>
-my $args = delete $session{QuickCreate} || {};
+my $args = $session{QuickCreate} || {};
+RT::Interface::Web::Session::Delete(
+    SessionRef => \%session,
+    SessionKey => 'QuickCreate',
+);
 </%INIT>
diff --git a/share/html/Helpers/Upload/Delete b/share/html/Helpers/Upload/Delete
index 50fe73b23e..10808e3318 100644
--- a/share/html/Helpers/Upload/Delete
+++ b/share/html/Helpers/Upload/Delete
@@ -50,8 +50,12 @@ $Name => ''
 $Token => ''
 </%args>
 <%init>
-delete $session{'Attachments'}{ $Token }{ $Name };
-$session{'Attachments'} = $session{'Attachments'};
+RT::Interface::Web::Session::Delete(
+    SessionRef       => \%session,
+    SessionKey       => 'Attachments',
+    SessionSubKey    => $Token,
+    SessionSubSubKey => $Name,
+);
 $r->content_type('application/json; charset=utf-8');
 $m->out( JSON({status => 'success'}) );
 $m->abort;
diff --git a/share/html/Install/index.html b/share/html/Install/index.html
index 2d78882524..0c8bed97aa 100644
--- a/share/html/Install/index.html
+++ b/share/html/Install/index.html
@@ -128,6 +128,8 @@ elsif ( $Run ) {
 
     RT::Interface::Web::Redirect(RT->Config->Get('WebURL') . 'Install/DatabaseType.html');
 } elsif ( $ChangeLang && $Lang ) {
+    # Don't call RT::Interface::Web::Session::Set because if we're
+    # in the installer, we don't have a DB.
     # hackish, but works
     $session{'CurrentUser'} = RT::CurrentUser->new;
     $session{'CurrentUser'}->LanguageHandle( $Lang );
diff --git a/share/html/NoAuth/Logout.html b/share/html/NoAuth/Logout.html
index f24b172129..459835b3d6 100644
--- a/share/html/NoAuth/Logout.html
+++ b/share/html/NoAuth/Logout.html
@@ -91,7 +91,11 @@ if (keys %session) {
 
     # Clear the session
     RT::Interface::Web::InstantiateNewSession();
-    $session{'CurrentUser'} = RT::CurrentUser->new;
+    RT::Interface::Web::Session::Set(
+        SessionRef   => \%session,
+        SessionKey   => 'CurrentUser',
+        SessionValue => RT::CurrentUser->new,
+    );
 
     if ( $externally_authed ) {
         # For SAML-type auth, there is another session which will need to be
diff --git a/share/html/Prefs/DashboardsInMenu.html b/share/html/Prefs/DashboardsInMenu.html
index 26a9b72caf..8e179a4ae8 100644
--- a/share/html/Prefs/DashboardsInMenu.html
+++ b/share/html/Prefs/DashboardsInMenu.html
@@ -107,7 +107,10 @@ if ( $ARGS{ResetDashboards} ) {
     # Empty DashboardsInMenu pref means to use system default.
     my ($ok, $msg) = $user->SetPreferences('DashboardsInMenu', {});
     push @results, $ok ? loc('Preferences saved.') : $msg;
-    delete $session{'dashboards_in_menu'};
+    RT::Interface::Web::Session::Delete(
+        SessionRef => \%session,
+        SessionKey => 'dashboards_in_menu',
+    );
 }
 
 if ( $ARGS{ResetReports} ) {
@@ -116,7 +119,10 @@ if ( $ARGS{ResetReports} ) {
         # thus we need to delete preference instead.
         my ( $ok, $msg ) = $user->DeletePreferences('ReportsInMenu');
         push @results, $ok ? loc('Preferences saved.') : $msg;
-        delete $session{'reports_in_menu'};
+        RT::Interface::Web::Session::Delete(
+            SessionRef => \%session,
+            SessionKey => 'reports_in_menu',
+        );
     }
 }
 
@@ -146,7 +152,10 @@ if ($ARGS{UpdateSearches}) {
 
         my ( $ok, $msg ) = $user->SetPreferences( $ARGS{'dashboard_id'}, { 'dashboards' => \@dashboard_ids } );
         push @results, $ok ? loc('Preferences saved for dashboards in menu.') : $msg;
-        delete $session{'dashboards_in_menu'};
+        RT::Interface::Web::Session::Delete(
+            SessionRef => \%session,
+            SessionKey => 'dashboards_in_menu',
+        );
     }
     else {
       my $report_names = ref $ARGS{'report'} eq 'ARRAY' ? $ARGS{'report'} : [$ARGS{'report'}];
@@ -159,7 +168,10 @@ if ($ARGS{UpdateSearches}) {
 
       my ( $ok, $msg ) = $user->SetPreferences( $ARGS{'dashboard_id'}, \@ret );
       push @results, $ok ? loc('Preferences saved for reports in menu.') : $msg;
-      delete $session{'reports_in_menu'};
+      RT::Interface::Web::Session::Delete(
+          SessionRef => \%session,
+          SessionKey => 'reports_in_menu',
+      );
     }
 }
 
diff --git a/share/html/Prefs/QueueList.html b/share/html/Prefs/QueueList.html
index 5dca1630e7..d34b0a52b8 100644
--- a/share/html/Prefs/QueueList.html
+++ b/share/html/Prefs/QueueList.html
@@ -112,12 +112,18 @@ if ($ARGS{'Save'}) {
         # Clear for 'CreateTicket'
         my $cache_key = GetObjectSessionCacheKey( ObjectType => 'RT::Queue',
             CheckRight => 'CreateTicket', ShowAll => 0 );
-        delete $session{$cache_key};
+        RT::Interface::Web::Session::Delete(
+            SessionRef => \%session,
+            SessionKey => $cache_key,
+        );
 
         # Clear for 'ShowTicket'
         $cache_key = GetObjectSessionCacheKey( ObjectType => 'RT::Queue',
             CheckRight => 'ShowTicket', ShowAll => 0 );
-        delete $session{$cache_key};
+        RT::Interface::Web::Session::Delete(
+            SessionRef => \%session,
+            SessionKey => $cache_key,
+        );
     }
 }
 
diff --git a/share/html/REST/1.0/logout b/share/html/REST/1.0/logout
index fb2fe11b51..ed1d94cc04 100644
--- a/share/html/REST/1.0/logout
+++ b/share/html/REST/1.0/logout
@@ -48,7 +48,11 @@
 <%PERL>
 if (keys %session) {
     RT::Interface::Web::InstantiateNewSession();
-    $session{CurrentUser} = RT::CurrentUser->new();
+    RT::Interface::Web::Session::Set(
+        SessionRef   => \%session,
+        SessionKey   => 'CurrentUser',
+        SessionValue => RT::CurrentUser->new,
+    );
 }
 </%PERL>
 RT/<% $RT::VERSION %> 200 Ok
diff --git a/share/html/Search/Build.html b/share/html/Search/Build.html
index 3c1028926b..773e83bff2 100644
--- a/share/html/Search/Build.html
+++ b/share/html/Search/Build.html
@@ -168,7 +168,10 @@ if ( $NewQuery ) {
     %saved_search = ( Id => 'new' );
 
     # ..then wipe the session out..
-    delete $session{$hash_name};
+    RT::Interface::Web::Session::Delete(
+        SessionRef => \%session,
+        SessionKey => $hash_name,
+    );
 
     # ..and the search results.
     $session{$session_name}->CleanSlate if defined $session{$session_name};
@@ -349,12 +352,16 @@ if ($ARGS{SavedSearchSave}) {
 
 # Push the updates into the session so we don't lose 'em
 
-$session{$hash_name} = {
-    %query,
-    SearchId    => $saved_search{'Id'},
-    Object      => $saved_search{'Object'},
-    Description => $saved_search{'Description'},
-};
+RT::Interface::Web::Session::Set(
+    SessionRef   => \%session,
+    SessionKey   => $hash_name,
+    SessionValue => {
+        %query,
+        SearchId    => $saved_search{'Id'},
+        Object      => $saved_search{'Object'},
+        Description => $saved_search{'Description'},
+    },
+);
 
 
 # Show the results, if we were asked.
diff --git a/share/html/Search/Bulk.html b/share/html/Search/Bulk.html
index 2c82016922..f8924e7fad 100644
--- a/share/html/Search/Bulk.html
+++ b/share/html/Search/Bulk.html
@@ -428,7 +428,11 @@ unless ( $ARGS{'AddMoreAttach'} ) {
     }
     $RT::Handle->Commit;
 
-    delete $session{'Attachments'}{ $ARGS{'Token'} };
+    RT::Interface::Web::Session::Delete(
+        SessionRef    => \%session,
+        SessionKey    => 'Attachments',
+        SessionSubKey => $ARGS{'Token'},
+    );
 
     $Tickets->RedoSearch();
 }
diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index acc121d5c8..f718a5be2e 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -105,10 +105,18 @@ use RT::Report::Tickets;
 my $report = RT::Report::Tickets->new( $session{'CurrentUser'} );
 
 my %columns;
-if ( $Cache and my $data = delete $session{'charts_cache'}{ $Cache } ) {
-    %columns = %{ $data->{'columns'} };
-    $report->Deserialize( $data->{'report'} );
-    $session{'i'}++;
+if ( $Cache ) {
+    my $data;
+    if ( $session{'charts_cache'}{ $Cache } ) {
+        $data = $session{'charts_cache'}{ $Cache };
+        RT::Interface::Web::Session::Delete(
+            SessionRef    => \%session,
+            SessionKey    => 'charts_cache',
+            SessionSubKey => $Cache,
+        );
+        %columns = %{ $data->{'columns'} };
+        $report->Deserialize( $data->{'report'} );
+    }
 } else {
     %columns = $report->SetupGroupings(
         Query => $Query,
diff --git a/share/html/Search/Elements/Chart b/share/html/Search/Elements/Chart
index 021a07cfb7..a94d23343e 100644
--- a/share/html/Search/Elements/Chart
+++ b/share/html/Search/Elements/Chart
@@ -69,8 +69,13 @@ my $query_string = $m->comp('/Elements/QueryString', %ARGS, GroupBy => \@GroupBy
 my $key;
 if ( RT->Config->Get('EnableJSChart') || !RT->Config->Get('DisableGD') ) {
     $key = Digest::MD5::md5_hex( rand(1024) );
-    $session{'charts_cache'}{$key} = { columns => \%columns, report => $report->Serialize };
-    $session{'i'}++;
+    RT::Interface::Web::Session::Set(
+        SessionRef    => \%session,
+        SessionKey    => 'charts_cache',
+        SessionSubKey => $key,
+        SessionValue  => { columns => \%columns, report => $report->Serialize },
+    );
+
 }
 
 </%init>
diff --git a/share/html/Search/JSChart b/share/html/Search/JSChart
index 9c1733f548..410362b3d0 100644
--- a/share/html/Search/JSChart
+++ b/share/html/Search/JSChart
@@ -196,10 +196,18 @@ my $report = RT::Report::Tickets->new( $session{'CurrentUser'} );
 @GroupBy = 'Status' unless @GroupBy;
 
 my %columns;
-if ( $Cache and my $data = delete $session{'charts_cache'}{ $Cache } ) {
-    %columns = %{ $data->{'columns'} };
-    $report->Deserialize( $data->{'report'} );
-    $session{'i'}++;
+if ( $Cache ) {
+    my $data;
+    if ( $session{'charts_cache'}{ $Cache } ) {
+        $data = $session{'charts_cache'}{ $Cache };
+        RT::Interface::Web::Session::Delete(
+            SessionRef    => \%session,
+            SessionKey    => 'charts_cache',
+            SessionSubKey => $Cache,
+        );
+        %columns = %{ $data->{'columns'} };
+        $report->Deserialize( $data->{'report'} );
+    }
 } else {
     %columns = $report->SetupGroupings(
         Query => $Query,
diff --git a/share/html/Search/Results.html b/share/html/Search/Results.html
index 1154e02dd8..d90e7807ed 100644
--- a/share/html/Search/Results.html
+++ b/share/html/Search/Results.html
@@ -171,8 +171,11 @@ $Page = 1 unless $Page && $Page > 0;
 my $hash_name = join '-', 'CurrentSearchHash', $Class, $ObjectType || ();
 my $session_name = join '-', 'collection', $Class, $ObjectType || ();
 
-$session{'i'}++;
-$session{$session_name} = $Class->new($session{'CurrentUser'}) ;
+RT::Interface::Web::Session::Set(
+    SessionRef   => \%session,
+    SessionKey   => $session_name,
+    SessionValue => $Class->new($session{'CurrentUser'}),
+);
 
 my ( $ok, $msg );
 if ( $Query ) {
@@ -205,16 +208,28 @@ $session{$session_name}->RowsPerPage( $Rows ) if $Rows;
 $session{$session_name}->GotoPage( $Page - 1 );
 $session{$session_name}->CombineSearchAndCount(1);
 
-$session{$hash_name} = {
-    Format      => $Format,
-    Query       => $Query,
-    Page        => $Page,
-    Order       => $Order,
-    OrderBy     => $OrderBy,
-    RowsPerPage => $Rows,
-    ObjectType  => $ObjectType,
-};
+# Save the session again because we made changes
+# Otherwise the set below will restore it back to
+# the last set.
+RT::Interface::Web::Session::Set(
+    SessionRef   => \%session,
+    SessionKey   => $session_name,
+    SessionValue => $session{$session_name},
+);
 
+RT::Interface::Web::Session::Set(
+    SessionRef   => \%session,
+    SessionKey   => $hash_name,
+    SessionValue => {
+        Format      => $Format,
+        Query       => $Query,
+        Page        => $Page,
+        Order       => $Order,
+        OrderBy     => $OrderBy,
+        RowsPerPage => $Rows,
+        ObjectType  => $ObjectType,
+    },
+);
 
 my $count = $session{$session_name}->Query() ? $session{$session_name}->CountAll() : 0;
 
@@ -255,7 +270,11 @@ my $ShortQueryString = "?".$m->comp('/Elements/QueryString', Query => $Query);
 
 my $interval_name = join '_', $Class, $ObjectType || (), 'refresh_interval';
 if ($ARGS{'SearchResultsRefreshInterval'}) {
-    $session{$interval_name} = $ARGS{'SearchResultsRefreshInterval'};
+    RT::Interface::Web::Session::Set(
+        SessionRef   => \%session,
+        SessionKey   => $interval_name,
+        SessionValue => $ARGS{'SearchResultsRefreshInterval'},
+    );
 }
 my $refresh = $session{$interval_name} || RT->Config->Get('SearchResultsRefreshInterval', $session{'CurrentUser'} );
 
diff --git a/share/html/SelfService/Elements/RequestUpdate b/share/html/SelfService/Elements/RequestUpdate
index 75fa89524e..858446c060 100644
--- a/share/html/SelfService/Elements/RequestUpdate
+++ b/share/html/SelfService/Elements/RequestUpdate
@@ -68,7 +68,11 @@ action="<%RT->Config->Get('WebPath')%><% $r->path_info %>"
 </div>
 
 <%INIT>
-my $args = delete $session{QuickCreate} || {};
+my $args = $session{QuickCreate} || {};
+RT::Interface::Web::Session::Delete(
+    SessionRef => \%session,
+    SessionKey => 'QuickCreate',
+);
 </%INIT>
 
 <%ARGS>
diff --git a/share/html/SelfService/Helpers/Upload/Delete b/share/html/SelfService/Helpers/Upload/Delete
index 50fe73b23e..10808e3318 100644
--- a/share/html/SelfService/Helpers/Upload/Delete
+++ b/share/html/SelfService/Helpers/Upload/Delete
@@ -50,8 +50,12 @@ $Name => ''
 $Token => ''
 </%args>
 <%init>
-delete $session{'Attachments'}{ $Token }{ $Name };
-$session{'Attachments'} = $session{'Attachments'};
+RT::Interface::Web::Session::Delete(
+    SessionRef       => \%session,
+    SessionKey       => 'Attachments',
+    SessionSubKey    => $Token,
+    SessionSubSubKey => $Name,
+);
 $r->content_type('application/json; charset=utf-8');
 $m->out( JSON({status => 'success'}) );
 $m->abort;
diff --git a/share/html/SelfService/Prefs.html b/share/html/SelfService/Prefs.html
index 26166aa80a..b41c4edbfd 100644
--- a/share/html/SelfService/Prefs.html
+++ b/share/html/SelfService/Prefs.html
@@ -155,7 +155,11 @@ if ( $pref eq 'edit-prefs' || $pref eq 'edit-prefs-view-info' || $pref eq 'full-
 
     if ( $Lang ) {
         $session{'CurrentUser'}->LanguageHandle($Lang);
-        $session{'CurrentUser'} = $session{'CurrentUser'}; # force writeback
+        RT::Interface::Web::Session::Set(
+            SessionRef   => \%session,
+            SessionKey   => 'CurrentUser',
+            SessionValue => $session{'CurrentUser'},
+        );
     }
 }
 
diff --git a/share/html/Ticket/Create.html b/share/html/Ticket/Create.html
index 10c63c46d1..a8eb4875d8 100644
--- a/share/html/Ticket/Create.html
+++ b/share/html/Ticket/Create.html
@@ -363,7 +363,11 @@ unless ($Queue) {
 
 Abort( loc( "Permission Denied" ) ) unless $Queue;
 
-$session{DefaultQueue} = $Queue;
+RT::Interface::Web::Session::Set(
+    SessionRef   => \%session,
+    SessionKey   => 'DefaultQueue',
+    SessionValue => $Queue,
+);
 
 my $current_user = $session{'CurrentUser'};
 
diff --git a/share/html/Widgets/SavedSearch b/share/html/Widgets/SavedSearch
index 0359ae752d..975fd94a16 100644
--- a/share/html/Widgets/SavedSearch
+++ b/share/html/Widgets/SavedSearch
@@ -82,7 +82,10 @@ if ( my ( $container_object, $search_id ) = _parse_saved_search(
 # need to delete $session{CurrentSearchHash} to let it not show the old one.
 # of course, the new one should not be shown there either because it's of
 # different type
-    delete $session{'CurrentSearchHash'};
+    RT::Interface::Web::Session::Delete(
+        SessionRef => \%session,
+        SessionKey => 'CurrentSearchHash',
+    );
 }
 
 # look for the current one in the available saved searches
diff --git a/share/html/m/logout b/share/html/m/logout
index bd27b06614..b2e1c5360b 100644
--- a/share/html/m/logout
+++ b/share/html/m/logout
@@ -48,7 +48,11 @@
 <%init>
 if (keys %session) {
     RT::Interface::Web::InstantiateNewSession();
-    $session{'CurrentUser'} = RT::CurrentUser->new;
+    RT::Interface::Web::Session::Set(
+        SessionRef   => \%session,
+        SessionKey   => 'CurrentUser',
+        SessionValue => RT::CurrentUser->new,
+    );
 }
 RT::Interface::Web::Redirect(RT->Config->Get('WebURL')."m/");
 </%init>
diff --git a/t/web/session.t b/t/web/session.t
index b7b7d1dfdc..ce450c1314 100644
--- a/t/web/session.t
+++ b/t/web/session.t
@@ -36,7 +36,11 @@ my ($session_id) = $agent->cookie_jar->as_string =~ /RT_SID_[^=]+=(\w+);/;
 
 diag 'Load session for root user';
 my %session;
-tie %session, 'RT::Interface::Web::Session', $session_id;
+RT::Interface::Web::Session::Load(
+    SessionRef => \%session,
+    SessionId  => $session_id,
+);
+
 is ( $session{'_session_id'}, $session_id, 'Got session id ' . $session_id );
 is ( $session{'CurrentUser'}->Name, 'root', 'Session is for root user' );
 
@@ -49,16 +53,70 @@ is ( $session{'SelectObject---RT::Queue---' . $user_id . '---CreateTicket---0'}{
 my $last_updated = $session{'SelectObject---RT::Queue---' . $user_id . '---CreateTicket---0'}{'lastupdated'};
 ok( $last_updated, "Got a lastupdated timestamp of $last_updated");
 
-untie(%session);
 # Wait for 1 sec so we can confirm lastupdated doesn't change
 sleep 1;
 $agent->get($url);
 is ($agent->status, 200, "Loaded a page");
 
-tie %session, 'RT::Interface::Web::Session', $session_id;
+RT::Interface::Web::Session::Load(
+    SessionRef => \%session,
+    SessionId  => $session_id,
+);
+
 is ( $session{'_session_id'}, $session_id, 'Got session id ' . $session_id );
 is ( $session{'CurrentUser'}->Name, 'root', 'Session is for root user' );
 is ($last_updated, $session{'SelectObject---RT::Queue---' . $user_id . '---CreateTicket---0'}{'lastupdated'},
     "lastupdated is still $last_updated");
 
+RT::Interface::Web::Session::Set(
+    SessionRef   => \%session,
+    SessionKey   => 'Testing',
+    SessionValue => 'TestValue',
+);
+
+is ( $session{'Testing'}, 'TestValue', 'Set a test value' );
+
+RT::Interface::Web::Session::Load(
+    SessionRef => \%session,
+    SessionId  => $session_id,
+);
+
+is ( $session{'Testing'}, 'TestValue', 'Test value still set after Load' );
+
+RT::Interface::Web::Session::Delete(
+    SessionRef => \%session,
+    SessionKey => 'Testing',
+);
+
+ok ( !(exists $session{'Testing'}), 'Test value deleted' );
+
+RT::Interface::Web::Session::Load(
+    SessionRef => \%session,
+    SessionId  => $session_id,
+);
+
+ok ( !(exists $session{'Testing'}), 'Test value still deleted after Load' );
+
+diag 'Test logging out';
+
+# Log in again first
+ok ( $agent->logout(), 'Logged out' );
+$agent->login('root' => 'password');
+# the field isn't named, so we have to click link 0
+is( $agent->status, 200, "Fetched the page ok");
+$agent->content_contains("Logout", "Found a logout link");
+
+my ($session_id2) = $agent->cookie_jar->as_string =~ /RT_SID_[^=]+=(\w+);/;
+
+ok ( $agent->logout(), 'Logged out' );
+
+RT::Interface::Web::Session::Load(
+    SessionRef => \%session,
+    SessionId  => $session_id2,
+);
+
+isnt ( $session{'_session_id'}, $session_id, 'Got a new session id' );
+ok ( !( exists $session{'CurrentUser'} ), 'New session is empty' );
+
+
 done_testing;

-----------------------------------------------------------------------


hooks/post-receive
-- 
rt


More information about the rt-commit mailing list