[Rt-commit] rt branch 5.0/non-blocking-sessions created. rt-5.0.3-82-ge71cca58e7

BPS Git Server git at git.bestpractical.com
Wed Aug 31 19:48:39 UTC 2022


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, 5.0/non-blocking-sessions has been created
        at  e71cca58e710ea1bfb40f984ddc4f82fd51d2d06 (commit)

- Log -----------------------------------------------------------------
commit e71cca58e710ea1bfb40f984ddc4f82fd51d2d06
Author: Jim Brandt <jbrandt at bestpractical.com>
Date:   Fri Aug 19 16:56:21 2022 -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 7384c92a91..c5148dad76 100644
--- a/lib/RT/Authen/ExternalAuth.pm
+++ b/lib/RT/Authen/ExternalAuth.pm
@@ -445,6 +445,12 @@ sub DoAuth {
             $session->{'CurrentUser'}->Load($UserObj->Id);
         }
 
+        RT::Interface::Web::Session::Set(
+            SessionRef   => \%HTML::Mason::Commands::session,
+            SessionKey   => 'CurrentUser',
+            SessionValue => $session->{'CurrentUser'},
+        );
+
         ####################################################################
         ########## Authentication ##########################################
         ####################################################################
@@ -475,11 +481,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");
     }
 
@@ -515,6 +531,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");
         }
     }
@@ -534,9 +555,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 5eca55a391..9311bad787 100644
--- a/lib/RT/Dashboard/Mailer.pm
+++ b/lib/RT/Dashboard/Mailer.pm
@@ -335,6 +335,7 @@ SUMMARY
 
     local $HTML::Mason::Commands::session{CurrentUser} = $currentuser;
     local $HTML::Mason::Commands::session{ContextUser} = $context_user;
+    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 6e827ffe28..2be9c0be34 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -347,6 +347,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' ) || [] };
@@ -372,7 +379,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() ) {
@@ -411,8 +422,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' );
@@ -430,7 +446,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 {
@@ -451,8 +470,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;
 }
 
@@ -488,8 +514,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;
 }
 
@@ -501,6 +532,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};
 }
 
@@ -512,7 +548,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]
@@ -771,6 +813,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
@@ -799,6 +847,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" );
@@ -806,7 +859,11 @@ sub AttemptExternalAuth {
         }
 
         if ( _UserLoggedIn() ) {
-            $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
@@ -889,7 +946,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 );
 
@@ -924,7 +986,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.
@@ -957,7 +1023,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();
     }
@@ -970,13 +1041,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();
 }
 
@@ -1023,10 +1106,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) {
@@ -1662,8 +1744,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+(/|$)};
@@ -1729,8 +1817,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;
@@ -1759,8 +1853,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;
 }
 
@@ -2198,8 +2297,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;
     }
 
@@ -2316,10 +2426,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'} };
@@ -2452,11 +2565,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}{$_},
@@ -2624,14 +2739,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
@@ -2663,11 +2780,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;
 }
 
@@ -4192,7 +4313,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,
@@ -4608,16 +4735,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} ) {
@@ -4628,24 +4764,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 a6ddccad8e..2d4c8c456f 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 f5e91da737..10135506fb 100644
--- a/share/html/Asset/Create.html
+++ b/share/html/Asset/Create.html
@@ -118,7 +118,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 187cfd0b80..41256dc919 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 50 rows
 # $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 00ee3bd593..4ec6ec4f8d 100644
--- a/share/html/Elements/QuickCreate
+++ b/share/html/Elements/QuickCreate
@@ -94,5 +94,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 9fb50a7da4..7508605d40 100644
--- a/share/html/Install/index.html
+++ b/share/html/Install/index.html
@@ -135,6 +135,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 4587533211..21855c4f87 100644
--- a/share/html/NoAuth/Logout.html
+++ b/share/html/NoAuth/Logout.html
@@ -79,7 +79,11 @@ $m->callback( %ARGS, CallbackName => 'BeforeSessionDelete' );
 
 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,
+    );
 }
 
 $m->callback( %ARGS, CallbackName => 'AfterSessionDelete' );
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 2f3be5f914..c02b6cdf92 100644
--- a/share/html/Search/Build.html
+++ b/share/html/Search/Build.html
@@ -172,7 +172,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};
@@ -351,12 +354,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 68767c4f31..e8882052db 100644
--- a/share/html/Search/Bulk.html
+++ b/share/html/Search/Bulk.html
@@ -557,7 +557,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 bc3f9d3ac2..1b24936ce0 100644
--- a/share/html/Search/JSChart
+++ b/share/html/Search/JSChart
@@ -199,10 +199,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 e0de84d99f..71b2869176 100644
--- a/share/html/Search/Results.html
+++ b/share/html/Search/Results.html
@@ -164,8 +164,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 ) {
@@ -197,16 +200,28 @@ if ($OrderBy =~ /\|/) {
 $session{$session_name}->RowsPerPage( $Rows ) if $Rows;
 $session{$session_name}->GotoPage( $Page - 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;
 
@@ -247,7 +262,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 fbb1972263..7dc54cdc4c 100644
--- a/share/html/SelfService/Elements/RequestUpdate
+++ b/share/html/SelfService/Elements/RequestUpdate
@@ -82,7 +82,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 128d534847..54bc1ffcf9 100644
--- a/share/html/SelfService/Prefs.html
+++ b/share/html/SelfService/Prefs.html
@@ -169,7 +169,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 6af9f0bee7..4cd8018910 100644
--- a/share/html/Ticket/Create.html
+++ b/share/html/Ticket/Create.html
@@ -368,7 +368,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