[Rt-commit] rt branch master updated. rt-5.0.3-501-gceaa4db4f5

BPS Git Server git at git.bestpractical.com
Wed Apr 26 20:54:35 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, master has been updated
       via  ceaa4db4f53d55208c18c00307c5518e23cdd77c (commit)
       via  5608bd4a2e856402f664c58a3a05d32f51d8452d (commit)
       via  3b13cf030b40a1dc190d5ea16eb5ea9afe4f8acd (commit)
      from  c28318a3247768bda6b98aa69853cb6155022adc (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.

- Log -----------------------------------------------------------------
commit ceaa4db4f53d55208c18c00307c5518e23cdd77c
Merge: c28318a324 5608bd4a2e
Author: sunnavy <sunnavy at bestpractical.com>
Date:   Thu Apr 27 04:50:56 2023 +0800

    Merge branch '6.0/non-blocking-sessions'


commit 5608bd4a2e856402f664c58a3a05d32f51d8452d
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 3b13cf030b40a1dc190d5ea16eb5ea9afe4f8acd
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..98349921c5 100644
--- a/lib/RT/Authen/ExternalAuth.pm
+++ b/lib/RT/Authen/ExternalAuth.pm
@@ -446,6 +446,11 @@ sub DoAuth {
             $session->{'CurrentUser'}->Load($UserObj->Id);
         }
 
+        RT::Interface::Web::Session::Set(
+            Key   => 'CurrentUser',
+            Value => $session->{'CurrentUser'},
+        );
+
         ####################################################################
         ########## Authentication ##########################################
         ####################################################################
@@ -476,11 +481,19 @@ 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(
+            Key   => 'CurrentUser',
+            Value => $session->{'CurrentUser'},
+        );
         return (0, "No User");
     }
 
     unless($success) {
         $session->{'CurrentUser'} = RT::CurrentUser->new;
+        RT::Interface::Web::Session::Set(
+            Key   => 'CurrentUser',
+            Value => $session->{'CurrentUser'},
+        );
         return (0, "Password Invalid");
     }
 
@@ -516,6 +529,10 @@ 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(
+                Key   => 'CurrentUser',
+                Value => $session->{'CurrentUser'},
+            );
             return (0, "User account disabled, login denied");
         }
     }
@@ -535,9 +552,17 @@ sub DoAuth {
             my $cu = $session->{CurrentUser};
             RT::Interface::Web::InstantiateNewSession();
             $session->{CurrentUser} = $cu;
+            RT::Interface::Web::Session::Set(
+                Key   => 'CurrentUser',
+                Value => $session->{'CurrentUser'},
+            );
     } else {
             # Make SURE the session is purged to an empty user.
             $session->{'CurrentUser'} = RT::CurrentUser->new;
+            RT::Interface::Web::Session::Set(
+                Key   => 'CurrentUser',
+                Value => $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..bcdf354ada 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -355,6 +355,12 @@ sub HandleRequest {
         $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new();
     }
 
+    # Write changes back to persistent session
+    RT::Interface::Web::Session::Set(
+        Key   => 'CurrentUser',
+        Value => $HTML::Mason::Commands::session{'CurrentUser'},
+    );
+
     # attempt external auth
     $HTML::Mason::Commands::m->comp( '/Elements/DoAuth', %$ARGS )
         if @{ RT->Config->Get( 'ExternalAuthPriority' ) || [] };
@@ -380,7 +386,10 @@ 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(
+            Key   => 'NotMobile',
+            Value => 1,
+        );
     }
 
     unless ( _UserLoggedIn() ) {
@@ -419,8 +428,12 @@ 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(
+            Key   => 'home_refresh_interval',
+            Value => $ARGS->{'HomeRefreshInterval'},
+        );
+    }
 
     # Process per-page global callbacks
     $HTML::Mason::Commands::m->callback( %$ARGS, CallbackName => 'Default', CallbackPage => '/autohandler' );
@@ -438,7 +451,9 @@ sub HandleRequest {
 
 sub _ForceLogout {
 
-    delete $HTML::Mason::Commands::session{'CurrentUser'};
+    RT::Interface::Web::Session::Delete(
+        Key => 'CurrentUser',
+    );
 }
 
 sub _UserLoggedIn {
@@ -459,8 +474,14 @@ 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(
+        Key   => 'Actions',
+        Value => \@actions,
+    );
+
     return $key;
 }
 
@@ -496,8 +517,12 @@ sub SetNextPage {
         }
     }
 
-    $HTML::Mason::Commands::session{'NextPage'}->{$hash} = $page;
-    $HTML::Mason::Commands::session{'i'}++;
+    RT::Interface::Web::Session::Set(
+        Key    => 'NextPage',
+        SubKey => $hash,
+        Value  => $page,
+    );
+
     return $hash;
 }
 
@@ -509,6 +534,10 @@ Returns the stashed next page hashref for the given hash.
 
 sub FetchNextPage {
     my $hash = shift || "";
+    RT::Interface::Web::Session::Load(
+        Id => $HTML::Mason::Commands::session{'_session_id'},
+    );
+
     return $HTML::Mason::Commands::session{'NextPage'}->{$hash};
 }
 
@@ -520,7 +549,12 @@ 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(
+        Key    => 'NextPage',
+        SubKey => $hash,
+    );
+    return $return_hash;
 }
 
 =head2 TangentForLogin ARGSRef [HASH]
@@ -781,6 +815,11 @@ sub AttemptExternalAuth {
         $HTML::Mason::Commands::session{'CurrentUser'} = RT::CurrentUser->new();
         $HTML::Mason::Commands::session{'CurrentUser'}->$load_method($user);
 
+        RT::Interface::Web::Session::Set(
+            Key   => 'CurrentUser',
+            Value => $HTML::Mason::Commands::session{'CurrentUser'},
+        );
+
         if ( RT->Config->Get('WebRemoteUserAutocreate') and not _UserLoggedIn() ) {
 
             # Create users on-the-fly
@@ -809,6 +848,10 @@ 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(
+                    Key   => 'CurrentUser',
+                    Value => $HTML::Mason::Commands::session{'CurrentUser'},
+                );
             } else {
                 RT->Logger->error("Couldn't auto-create user '$user' when attempting WebRemoteUser: $msg");
                 AbortExternalAuth( Error => "UserAutocreateDefaultsOnLogin" );
@@ -817,7 +860,12 @@ 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(
+                Key   => 'WebExternallyAuthed',
+                Value => 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 +948,11 @@ sub AttemptPasswordAuthentication {
            $next = $next->{'url'} if ref $next;
 
         InstantiateNewSession();
-        $HTML::Mason::Commands::session{'CurrentUser'} = $user_obj;
+
+        RT::Interface::Web::Session::Set(
+            Key   => 'CurrentUser',
+            Value => $user_obj,
+        );
 
         $m->callback( %$ARGS, CallbackName => 'SuccessfulLogin', CallbackPage => '/autohandler', RedirectTo => \$next );
 
@@ -935,7 +987,10 @@ sub AttemptTokenAuthentication {
             $next = $next->{'url'} if ref $next;
 
             RT::Interface::Web::InstantiateNewSession();
-            $HTML::Mason::Commands::session{'CurrentUser'} = $user_obj;
+            RT::Interface::Web::Session::Set(
+                Key   => 'CurrentUser',
+                Value => $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 +1023,11 @@ 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(
+        Id => $SessionCookie,
+    );
+
     unless ( $SessionCookie && $HTML::Mason::Commands::session{'_session_id'} eq $SessionCookie ) {
         InstantiateNewSession();
     }
@@ -981,13 +1040,23 @@ 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(
+                Key   => '_session_last_update',
+                Value => $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();
+
+    RT::Interface::Web::Session::Load(
+        Id => undef,
+    );
+
     SendSessionCookie();
 }
 
@@ -1034,10 +1103,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 +1744,13 @@ 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(
+            Key   => 'REST',
+            Value => scalar( $path =~ m{^/+REST/\d+\.\d+(/|$)} ),
+        );
+    }
 
     if ($HTML::Mason::Commands::session{'REST'}) {
         return 0 if $path =~ m{^/+REST/\d+\.\d+(/|$)};
@@ -1743,8 +1816,13 @@ 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(
+            Key       => 'Attachments',
+            SubKey    => $ARGS->{'Token'}||'',
+            SubSubKey => $filename,
+            Value     => $mime,
+        );
     }
 
     return 1;
@@ -1773,8 +1851,12 @@ sub StoreRequestToken {
         };
     }
 
-    $HTML::Mason::Commands::session{'CSRF'}->{$token} = $data;
-    $HTML::Mason::Commands::session{'i'}++;
+    RT::Interface::Web::Session::Set(
+        Key    => 'CSRF',
+        SubKey => $token,
+        Value  => $data,
+    );
+
     return $token;
 }
 
@@ -2284,8 +2366,18 @@ 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(
+            Key    => 'Actions',
+            SubKey => $key,
+            Value  => $actions_ref,
+        );
+
         $arguments{'results'} = $key;
     }
 
@@ -2402,10 +2494,12 @@ 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(
+                Key    => 'Attachments',
+                SubKey => $ARGS{'Token'} || '',
+            );
+        }
     }
     if ( $ARGS{'Attachments'} ) {
         push @attachments, grep $_, map $ARGS{Attachments}->{$_}, sort keys %{ $ARGS{'Attachments'} };
@@ -2538,11 +2632,12 @@ 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(
+                Key    => 'Attachments',
+                SubKey => $args{'ARGSRef'}{'Token'} || '',
+            );
+        }
     }
     if ( $args{ARGSRef}{'UpdateAttachments'} ) {
         push @attachments, grep $_, map $args{ARGSRef}->{UpdateAttachments}{$_},
@@ -2710,14 +2805,15 @@ 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(
+                Key       => 'Attachments',
+                SubKey    => $token,
+                SubSubKey => $delete,
+            );
+        }
     }
 
     # store the uploaded attachment in session
@@ -2749,11 +2845,14 @@ sub ProcessAttachments {
             }
         }
 
-        $session{'Attachments'}{ $token }{ $file_path } = $attachment;
-
-        $update_session = 1;
+        RT::Interface::Web::Session::Set(
+            Key       => 'Attachments',
+            SubKey    => $token,
+            SubSubKey => $file_path,
+            Value     => $attachment,
+        );
     }
-    $session{'Attachments'} = $session{'Attachments'} if $update_session;
+
     return 1;
 }
 
@@ -3697,8 +3796,14 @@ 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(
+                    Key    => 'Actions',
+                    SubKey => '',
+                    Value  => $actions_ref,
+                );
                 $values[0] = $new_value;
             }
         }
@@ -4304,7 +4409,12 @@ sub ProcessQuickCreate {
             );
         }
 
-        $session{QuickCreate} = \%ARGS unless $created;
+        unless ( $created ) {
+            RT::Interface::Web::Session::Set(
+                Key   => 'QuickCreate',
+                Value => \%ARGS,
+            );
+        }
 
         MaybeRedirectForResults(
             Actions   => \@results,
@@ -4725,16 +4835,22 @@ sub SetObjectSessionCache {
         CheckRight => $CheckRight, ShowAll => $ShowAll );
 
     if ( defined $session{$cache_key} && !$session{$cache_key}{id} ) {
-        delete $session{$cache_key};
+        RT::Interface::Web::Session::Delete(
+            Key => $cache_key,
+        );
     }
 
     if ( defined $session{$cache_key}
          && ref $session{$cache_key} eq 'ARRAY') {
-        delete $session{$cache_key};
+         RT::Interface::Web::Session::Delete(
+             Key => $cache_key,
+         );
     }
     if ( defined $session{$cache_key} && defined $CacheNeedsUpdate &&
         $session{$cache_key}{lastupdated} <= $CacheNeedsUpdate ) {
-        delete $session{$cache_key};
+        RT::Interface::Web::Session::Delete(
+            Key => $cache_key,
+        );
     }
 
     if ( not defined $session{$cache_key} ) {
@@ -4745,24 +4861,34 @@ sub SetObjectSessionCache {
             CallbackPage => '/Elements/Quicksearch',
             ARGSRef => \%args, Collection => $collection, ObjectType => $ObjectType );
 
-        $session{$cache_key}{id} = {};
+        RT::Interface::Web::Session::Delete(
+            Key => $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(
+            Key   => $cache_key,
+            Value => \%ids,
+        );
+
     }
 
     return $cache_key;
diff --git a/lib/RT/Interface/Web/Session.pm b/lib/RT/Interface/Web/Session.pm
index 34fcd045d4..7be62613ab 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,187 @@ sub ClearByUser {
     $self->ClearOrphanLockFiles if $deleted;
 }
 
+=head3 Load
+
+Load a session or create a new one.
+
+Accepts: Ref, Id
+
+Ref is a reference to a hash which will be loaded with
+session data.
+
+Id 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 = (
+        Ref => \%HTML::Mason::Commands::session,
+        Id  => undef,
+        @_
+    );
+
+    my %local_session;
+    tie %local_session, 'RT::Interface::Web::Session', $args{'Id'};
+
+    # Use { %local_session } instead of \%local_session to not clone the tie part.
+    %{ $args{'Ref'} } = %{ Clone::clone( {%local_session} ) };
+
+    untie %local_session;
+
+    return 1;
+}
+
+=head3 Set
+
+Set a value in the session.
+
+Accepts: Ref, Key, SubKey, SubSubKey, Value
+
+Ref 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.
+
+Key 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.
+
+Value is the value to set in the indicated key.
+
+If _session_id is not set, it simply updates Ref.
+
+=cut
+
+sub Set {
+    my %args = (
+        Ref => \%HTML::Mason::Commands::session,
+        Key       => undef,
+        SubKey    => undef,
+        SubSubKey => undef,
+        Value     => undef,
+        @_
+    );
+
+    my $session_id = $args{'Ref'}->{'_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 Ref is a plain hashref.
+        $target = $args{'Ref'};
+    }
+
+    # Set the value, which will automagically set it in the back-end session storage
+    if ( defined $args{'SubSubKey'} ) {
+        $target->{ $args{'Key'} }{ $args{'SubKey'} }{ $args{'SubSubKey'} } = $args{'Value'};
+    }
+    elsif ( defined $args{'SubKey'} ) {
+        $target->{ $args{'Key'} }{ $args{'SubKey'} } = $args{'Value'};
+    }
+    else {
+        $target->{ $args{'Key'} } = $args{'Value'};
+    }
+
+    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{'Ref'} } = %{ 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: Ref, Key, SubKey, SubSubKey
+
+Ref 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.
+
+Key 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 Ref.
+
+=cut
+
+sub Delete {
+    my %args = (
+        Ref       => \%HTML::Mason::Commands::session,
+        Key       => undef,
+        SubKey    => undef,
+        SubSubKey => undef,
+        @_
+    );
+
+    my $session_id = $args{'Ref'}->{'_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 Ref is a plain hashref.
+        $target = $args{'Ref'};
+    }
+
+    if ( $args{'Key'} ) {
+
+        # Delete requested item from the session
+        if ( defined $args{'SubSubKey'} ) {
+            delete $target->{ $args{'Key'} }{ $args{'SubKey'} }{ $args{'SubSubKey'} };
+        }
+        elsif ( defined $args{'SubKey'} ) {
+            delete $target->{ $args{'Key'} }{ $args{'SubKey'} };
+        }
+        else {
+            delete $target->{ $args{'Key'} };
+        }
+
+        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{'Ref'} } = %{ Clone::clone( {%local_session} ) };
+        }
+    }
+    else {
+        # No key provided, delete the whole session
+        tied(%local_session)->delete if tied %local_session;
+        %{ $args{'Ref'} } = ();
+    }
+
+    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..554871414e 100644
--- a/share/html/Admin/Global/DashboardsInMenu.html
+++ b/share/html/Admin/Global/DashboardsInMenu.html
@@ -132,7 +132,9 @@ if ($ARGS{UpdateSearches}) {
                 );
             }
             push @actions, $ok ? loc('Global dashboards in menu saved.') : $msg;
-            delete $session{'dashboards_in_menu'};
+            RT::Interface::Web::Session::Delete(
+                Key => 'dashboards_in_menu',
+            );
         }
         else {
           my $report_names = ref $ARGS{'report'} eq 'ARRAY' ? $ARGS{'report'} : [$ARGS{'report'}];
@@ -155,7 +157,9 @@ if ($ARGS{UpdateSearches}) {
               );
           }
           push @actions, $ok ? loc('Preferences saved for reports in menu.') : $msg;
-          delete $session{'reports_in_menu'};
+          RT::Interface::Web::Session::Delete(
+              Key => 'reports_in_menu',
+          );
         }
     }
 }
diff --git a/share/html/Admin/Users/DashboardsInMenu.html b/share/html/Admin/Users/DashboardsInMenu.html
index fa4f0e248e..cee44aaf20 100644
--- a/share/html/Admin/Users/DashboardsInMenu.html
+++ b/share/html/Admin/Users/DashboardsInMenu.html
@@ -150,7 +150,9 @@ 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(
+          Key => 'dashboards_in_menu',
+      );
   }
   else {
     my $report_names = ref $ARGS{'report'} eq 'ARRAY' ? $ARGS{'report'} : [$ARGS{'report'}];
@@ -163,7 +165,9 @@ 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(
+        Key => 'reports_in_menu',
+    );
   }
 }
 
diff --git a/share/html/Asset/Create.html b/share/html/Asset/Create.html
index 61170a22e4..a3752726f9 100644
--- a/share/html/Asset/Create.html
+++ b/share/html/Asset/Create.html
@@ -114,7 +114,10 @@ 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(
+    Key   => 'DefaultCatalog',
+    Value => $catalog->Id,
+);
 
 my @results;
 
diff --git a/share/html/Asset/Elements/SelectCatalog b/share/html/Asset/Elements/SelectCatalog
index 0f003909c6..4b1792a7a3 100644
--- a/share/html/Asset/Elements/SelectCatalog
+++ b/share/html/Asset/Elements/SelectCatalog
@@ -65,7 +65,10 @@ $AutoSubmit     => 0
 <%init>
 my $catalog_obj = LoadDefaultCatalog($Default || '');
 if ( $UpdateSession && $catalog_obj->Id ){
-    $session{'DefaultCatalog'} = $catalog_obj->Id;
+    RT::Interface::Web::Session::Set(
+        Key   => 'DefaultCatalog',
+        Value => $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..6ef18b6178 100644
--- a/share/html/Dashboards/Render.html
+++ b/share/html/Dashboards/Render.html
@@ -151,7 +151,13 @@ for my $sub ($session{'CurrentUser'}->UserObj->Attributes->Named('Subscription')
     last;
 }
 
-$session{ContextUser} ||= $session{CurrentUser};
+if ( !$session{ContextUser} ) {
+   RT::Interface::Web::Session::Set(
+       Key   => 'ContextUser',
+       Value => $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..de03cbaf0d 100644
--- a/share/html/Elements/ListActions
+++ b/share/html/Elements/ListActions
@@ -61,20 +61,28 @@
 
 # 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(
+        Key => 'Actions',
+    );
 }
 
 if ( ref( $session{'Actions'}{''} ) eq 'ARRAY' ) {
-    unshift @actions, @{ delete $session{'Actions'}{''} };
-    $session{'i'}++;
+    unshift @actions, @{ $session{'Actions'}{''} };
+    RT::Interface::Web::Session::Delete(
+        Key    => 'Actions',
+        SubKey => '',
+    );
 }
 
 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(
+        Key    => 'Actions',
+        SubKey => $actions_pointer,
+    );
 }
 
 # XXX: run callbacks per row really crazy idea
diff --git a/share/html/Elements/QuickCreate b/share/html/Elements/QuickCreate
index 4def33febb..5a16bc65ca 100644
--- a/share/html/Elements/QuickCreate
+++ b/share/html/Elements/QuickCreate
@@ -81,5 +81,8 @@
 </div>
 
 <%INIT>
-my $args = delete $session{QuickCreate} || {};
+my $args = $session{QuickCreate} || {};
+RT::Interface::Web::Session::Delete(
+    Key => 'QuickCreate',
+);
 </%INIT>
diff --git a/share/html/Helpers/Upload/Delete b/share/html/Helpers/Upload/Delete
index 50fe73b23e..367891316b 100644
--- a/share/html/Helpers/Upload/Delete
+++ b/share/html/Helpers/Upload/Delete
@@ -50,8 +50,11 @@ $Name => ''
 $Token => ''
 </%args>
 <%init>
-delete $session{'Attachments'}{ $Token }{ $Name };
-$session{'Attachments'} = $session{'Attachments'};
+RT::Interface::Web::Session::Delete(
+    Key       => 'Attachments',
+    SubKey    => $Token,
+    SubSubKey => $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..c5957c82e3 100644
--- a/share/html/NoAuth/Logout.html
+++ b/share/html/NoAuth/Logout.html
@@ -91,7 +91,10 @@ if (keys %session) {
 
     # Clear the session
     RT::Interface::Web::InstantiateNewSession();
-    $session{'CurrentUser'} = RT::CurrentUser->new;
+    RT::Interface::Web::Session::Set(
+        Key   => 'CurrentUser',
+        Value => 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..4fb97e2932 100644
--- a/share/html/Prefs/DashboardsInMenu.html
+++ b/share/html/Prefs/DashboardsInMenu.html
@@ -107,7 +107,9 @@ 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(
+        Key => 'dashboards_in_menu',
+    );
 }
 
 if ( $ARGS{ResetReports} ) {
@@ -116,7 +118,9 @@ 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(
+            Key => 'reports_in_menu',
+        );
     }
 }
 
@@ -146,7 +150,9 @@ 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(
+            Key => 'dashboards_in_menu',
+        );
     }
     else {
       my $report_names = ref $ARGS{'report'} eq 'ARRAY' ? $ARGS{'report'} : [$ARGS{'report'}];
@@ -159,7 +165,9 @@ 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(
+          Key => 'reports_in_menu',
+      );
     }
 }
 
diff --git a/share/html/Prefs/QueueList.html b/share/html/Prefs/QueueList.html
index 5dca1630e7..617e940811 100644
--- a/share/html/Prefs/QueueList.html
+++ b/share/html/Prefs/QueueList.html
@@ -112,12 +112,16 @@ 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(
+            Key => $cache_key,
+        );
 
         # Clear for 'ShowTicket'
         $cache_key = GetObjectSessionCacheKey( ObjectType => 'RT::Queue',
             CheckRight => 'ShowTicket', ShowAll => 0 );
-        delete $session{$cache_key};
+        RT::Interface::Web::Session::Delete(
+            Key => $cache_key,
+        );
     }
 }
 
diff --git a/share/html/REST/1.0/logout b/share/html/REST/1.0/logout
index fb2fe11b51..546f5f23bf 100644
--- a/share/html/REST/1.0/logout
+++ b/share/html/REST/1.0/logout
@@ -48,7 +48,10 @@
 <%PERL>
 if (keys %session) {
     RT::Interface::Web::InstantiateNewSession();
-    $session{CurrentUser} = RT::CurrentUser->new();
+    RT::Interface::Web::Session::Set(
+        Key   => 'CurrentUser',
+        Value => 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..20b38e3fae 100644
--- a/share/html/Search/Build.html
+++ b/share/html/Search/Build.html
@@ -168,7 +168,9 @@ if ( $NewQuery ) {
     %saved_search = ( Id => 'new' );
 
     # ..then wipe the session out..
-    delete $session{$hash_name};
+    RT::Interface::Web::Session::Delete(
+        Key => $hash_name,
+    );
 
     # ..and the search results.
     $session{$session_name}->CleanSlate if defined $session{$session_name};
@@ -349,12 +351,15 @@ 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(
+    Key   => $hash_name,
+    Value => {
+        %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..4652da3ee1 100644
--- a/share/html/Search/Bulk.html
+++ b/share/html/Search/Bulk.html
@@ -428,7 +428,10 @@ unless ( $ARGS{'AddMoreAttach'} ) {
     }
     $RT::Handle->Commit;
 
-    delete $session{'Attachments'}{ $ARGS{'Token'} };
+    RT::Interface::Web::Session::Delete(
+        Key    => 'Attachments',
+        SubKey => $ARGS{'Token'},
+    );
 
     $Tickets->RedoSearch();
 }
diff --git a/share/html/Search/Chart b/share/html/Search/Chart
index acc121d5c8..0d0a07416f 100644
--- a/share/html/Search/Chart
+++ b/share/html/Search/Chart
@@ -105,10 +105,17 @@ 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(
+            Key    => 'charts_cache',
+            SubKey => $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..0913792bd9 100644
--- a/share/html/Search/Elements/Chart
+++ b/share/html/Search/Elements/Chart
@@ -69,8 +69,12 @@ 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(
+        Key    => 'charts_cache',
+        SubKey => $key,
+        Value  => { columns => \%columns, report => $report->Serialize },
+    );
+
 }
 
 </%init>
diff --git a/share/html/Search/JSChart b/share/html/Search/JSChart
index 9c1733f548..43b14e4e39 100644
--- a/share/html/Search/JSChart
+++ b/share/html/Search/JSChart
@@ -196,10 +196,17 @@ 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(
+            Key    => 'charts_cache',
+            SubKey => $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..b8a0fd3bba 100644
--- a/share/html/Search/Results.html
+++ b/share/html/Search/Results.html
@@ -171,8 +171,10 @@ $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(
+    Key   => $session_name,
+    Value => $Class->new($session{'CurrentUser'}),
+);
 
 my ( $ok, $msg );
 if ( $Query ) {
@@ -205,16 +207,26 @@ $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(
+    Key   => $session_name,
+    Value => $session{$session_name},
+);
 
+RT::Interface::Web::Session::Set(
+    Key   => $hash_name,
+    Value => {
+        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 +267,10 @@ 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(
+        Key   => $interval_name,
+        Value => $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..b169fd6115 100644
--- a/share/html/SelfService/Elements/RequestUpdate
+++ b/share/html/SelfService/Elements/RequestUpdate
@@ -68,7 +68,10 @@ action="<%RT->Config->Get('WebPath')%><% $r->path_info %>"
 </div>
 
 <%INIT>
-my $args = delete $session{QuickCreate} || {};
+my $args = $session{QuickCreate} || {};
+RT::Interface::Web::Session::Delete(
+    Key => 'QuickCreate',
+);
 </%INIT>
 
 <%ARGS>
diff --git a/share/html/SelfService/Helpers/Upload/Delete b/share/html/SelfService/Helpers/Upload/Delete
index 50fe73b23e..367891316b 100644
--- a/share/html/SelfService/Helpers/Upload/Delete
+++ b/share/html/SelfService/Helpers/Upload/Delete
@@ -50,8 +50,11 @@ $Name => ''
 $Token => ''
 </%args>
 <%init>
-delete $session{'Attachments'}{ $Token }{ $Name };
-$session{'Attachments'} = $session{'Attachments'};
+RT::Interface::Web::Session::Delete(
+    Key       => 'Attachments',
+    SubKey    => $Token,
+    SubSubKey => $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..729046e0de 100644
--- a/share/html/SelfService/Prefs.html
+++ b/share/html/SelfService/Prefs.html
@@ -155,7 +155,10 @@ 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(
+            Key   => 'CurrentUser',
+            Value => $session{'CurrentUser'},
+        );
     }
 }
 
diff --git a/share/html/Ticket/Create.html b/share/html/Ticket/Create.html
index 10c63c46d1..0628017c27 100644
--- a/share/html/Ticket/Create.html
+++ b/share/html/Ticket/Create.html
@@ -363,7 +363,10 @@ unless ($Queue) {
 
 Abort( loc( "Permission Denied" ) ) unless $Queue;
 
-$session{DefaultQueue} = $Queue;
+RT::Interface::Web::Session::Set(
+    Key   => 'DefaultQueue',
+    Value => $Queue,
+);
 
 my $current_user = $session{'CurrentUser'};
 
diff --git a/share/html/Ticket/Forward.html b/share/html/Ticket/Forward.html
index 4b42e29105..850729df1c 100644
--- a/share/html/Ticket/Forward.html
+++ b/share/html/Ticket/Forward.html
@@ -159,7 +159,14 @@ 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(
+            Key    => 'Actions',
+            SubKey => $key,
+            Value  => $actions_ref,
+        );
         RT::Interface::Web::Redirect( RT->Config->Get('WebURL') ."Ticket/Display.html?id=". $id."&results=".$key);
     }
 }
diff --git a/share/html/Widgets/SavedSearch b/share/html/Widgets/SavedSearch
index 0359ae752d..7b79b6bad5 100644
--- a/share/html/Widgets/SavedSearch
+++ b/share/html/Widgets/SavedSearch
@@ -82,7 +82,9 @@ 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(
+        Key => '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..ed60e863f0 100644
--- a/share/html/m/logout
+++ b/share/html/m/logout
@@ -48,7 +48,10 @@
 <%init>
 if (keys %session) {
     RT::Interface::Web::InstantiateNewSession();
-    $session{'CurrentUser'} = RT::CurrentUser->new;
+    RT::Interface::Web::Session::Set(
+        Key   => 'CurrentUser',
+        Value => 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..a04b287160 100644
--- a/t/web/session.t
+++ b/t/web/session.t
@@ -36,7 +36,10 @@ 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(
+    Id => $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 +52,64 @@ 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(
+    Id => $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(
+    Key   => 'Testing',
+    Value => 'TestValue',
+);
+
+is ( $session{'Testing'}, 'TestValue', 'Set a test value' );
+
+RT::Interface::Web::Session::Load(
+    Id => $session_id,
+);
+
+is ( $session{'Testing'}, 'TestValue', 'Test value still set after Load' );
+
+RT::Interface::Web::Session::Delete(
+    Key => 'Testing',
+);
+
+ok ( !(exists $session{'Testing'}), 'Test value deleted' );
+
+RT::Interface::Web::Session::Load(
+    Id => $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(
+    Id => $session_id2,
+);
+
+isnt ( $session{'_session_id'}, $session_id, 'Got a new session id' );
+ok ( !( exists $session{'CurrentUser'} ), 'New session is empty' );
+
+
 done_testing;

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

Summary of changes:
 lib/RT/Authen/ExternalAuth.pm                 |  25 +++
 lib/RT/Dashboard/Mailer.pm                    |   1 +
 lib/RT/Interface/Web.pm                       | 236 ++++++++++++++++++++------
 lib/RT/Interface/Web/Session.pm               | 182 ++++++++++++++++++++
 share/html/Admin/Global/DashboardsInMenu.html |   8 +-
 share/html/Admin/Users/DashboardsInMenu.html  |   8 +-
 share/html/Asset/Create.html                  |   5 +-
 share/html/Asset/Elements/SelectCatalog       |   5 +-
 share/html/Dashboards/Render.html             |   8 +-
 share/html/Elements/ListActions               |  20 ++-
 share/html/Elements/QuickCreate               |   5 +-
 share/html/Helpers/Upload/Delete              |   7 +-
 share/html/Install/index.html                 |   2 +
 share/html/NoAuth/Logout.html                 |   5 +-
 share/html/Prefs/DashboardsInMenu.html        |  16 +-
 share/html/Prefs/QueueList.html               |   8 +-
 share/html/REST/1.0/logout                    |   5 +-
 share/html/Search/Build.html                  |  19 ++-
 share/html/Search/Bulk.html                   |   5 +-
 share/html/Search/Chart                       |  15 +-
 share/html/Search/Elements/Chart              |   8 +-
 share/html/Search/JSChart                     |  15 +-
 share/html/Search/Results.html                |  39 +++--
 share/html/SelfService/Elements/RequestUpdate |   5 +-
 share/html/SelfService/Helpers/Upload/Delete  |   7 +-
 share/html/SelfService/Prefs.html             |   5 +-
 share/html/Ticket/Create.html                 |   5 +-
 share/html/Ticket/Forward.html                |   9 +-
 share/html/Widgets/SavedSearch                |   4 +-
 share/html/m/logout                           |   5 +-
 t/web/session.t                               |  57 ++++++-
 t/web/ticket_forward.t                        |   1 +
 32 files changed, 625 insertions(+), 120 deletions(-)


hooks/post-receive
-- 
rt


More information about the rt-commit mailing list