B4J Question [ABMaterial] Session variables in safari

PCastagnetti

Member
Licensed User
Longtime User
Last edited:

PCastagnetti

Member
Licensed User
Longtime User
Hi @mindful
thanks for your suggestions
Unfortunately I can not think to disable the cache because of the number of images of my project.

I tried your solution 2 on my iPad and effectively the Filter sub is fired only when in websocket is requested;session variables unfortunately remain= null


For now I remain at this solution
B4X:
Private Sub WebSocket_Connected (WebSocket1 As WebSocket)
    Log("Connected")
  
    ws = WebSocket1
    AppPage.SetWebSocket(ws)
    ' Prepare the page IMPORTANT!
  
  
    Dim info As ABMPlatform = AppPage.GetPlatform
    Dim TmpOs As String = info.OSFamily
    Log(TmpOs)
    If TmpOs.ToLowerCase="ios" And Not(ws.Session.HasAttribute("Refresh")) Then
        ws.Eval("location.reload(arguments[0])",Array As Object(True))          
        ws.Session.SetAttribute("Refresh",True)
        Log(ws.Session.Id)
        Return
    End If
performed in WebSocket_Connected of ABMApplication.


I will do further tests with ver.2.02 when available

thank you
 
Upvote 0

mindful

Active Member
Licensed User
Then I'm out of ideeas sorry

Will be waiting for AB to realease a new version to see, as now I am courios ...
 
Upvote 0

mindful

Active Member
Licensed User
PCastagnetti when the variable return null ... the Session Id is the same ? or does it change ?

Steps to test:
1. access the first page ... log session id
2. set variable x to value y
3. log session id
4. change page ... log session id on websocket_disconnect in old page and on websocket_connect in new page
5. read the variable x that should return null
6. log session id

You should also log the session id in the Filter sub ("/ws/*"):
req.GetSession
Log(req.GetSession.Id)
Return True

And please post back if the session id is the same when the variable returns null
 
Upvote 0

alwaysbusy

Expert
Licensed User
Longtime User
If it is for the images, why don't you use something like this:

B4X:
Dim rndValue As Int = Rnd(1, 100000000)

Dim img4 As ABMImage
img4.Initialize(page, "img4", "../images/sample-1.jpg?" & rndValue, 1)  ' <---
img4.IsResponsive=True
img4.IsMaterialBoxed=True
page.Cell(6,1).AddComponent(img4)

or even: Dim rndValue as String = DateTime.Now

You can use this everywhere where you want to reload a file.
 
Upvote 0

mindful

Active Member
Licensed User
AB maybe you should add a javascript for checking if cookies(session cookies), javascript and websockets are supported and enabled in the browser if it's not that hard...
 
Upvote 0

alwaysbusy

Expert
Licensed User
Longtime User
But doing this you will keep a reference of that websocket class in session so it will not be disposed. So if the user's internet connection never enstablishes the server will hold a reference of that websocket class untill the session will expire.
Not the websocket, just all the class variables you defined + the page object (although as page holds a reference to the websocket, I will have to investigate if this is released)

And yes, it will definitely not solve PCastagnettis problem, as I understand he wants the opposite (always reload).

You are getting me very confused @mindful (or maybe I should consider sleeping some time)

Anyways, here is the code that saves/restores the session variables. Maybe it will clarify some things (or point out where I go wrong).
B4X:
public void SaveSessionVariables(B4AClass me, HttpSessionWrapper session) {
     //pageFieldName = "_" + pageFieldName.toLowerCase();
     Field[] fields = me.getClass().getDeclaredFields();
     for (Field classField : fields) {
       if (classField!=null) {
         //BA.Log("Checking: " + classField.getName() + " " + Modifier.toString(classField.getModifiers()) + " " + classField.getType().getName());
         switch (classField.getType().getSimpleName().toLowerCase()) {
         case "websocket":
         case "abmpage":
         case "abmaterial":
         case "common":         
           break;
         default:
           String packageName = BA.packageName.toLowerCase();
           //BA.Log("PackageName: " + packageName);
           switch (classField.getName().toLowerCase()) {
           case "htsubs":
             break;
           default:
             if (classField.getType().getName().toLowerCase().startsWith(packageName)) {
               try {
                 @SuppressWarnings("rawtypes")
                 Class c = Class.forName(classField.getType().getName());
                 boolean HasHtSubs=false;
                 try {
                   c.getDeclaredField("htSubs");
                   HasHtSubs=true;
                 } catch (NoSuchFieldException nsfe) {
                  // intentionally ignored                   
                 }
                 if (HasHtSubs) {
                   try {
                     session.SetAttribute(classField.getName(), classField.get(me));
                     //BA.Log("Saving: " + classField.getName() + " " + Modifier.toString(classField.getModifiers()) + " " + classField.getType().getSimpleName());
                   } catch (IllegalArgumentException e) {
                     BA.Log("Failed saving: " + classField.getName() + " " + Modifier.toString(classField.getModifiers()) + " " + classField.getType().getSimpleName());
                     //e.printStackTrace();
                   } catch (IllegalAccessException e) {
                     BA.Log("Failed saving: " + classField.getName() + " " + Modifier.toString(classField.getModifiers()) + " " + classField.getType().getSimpleName());
                     //e.printStackTrace();
                   }
                 }
               } catch (ClassNotFoundException e1) {
                 e1.printStackTrace();
               }
             
             } else {
               try {
                 session.SetAttribute(classField.getName(), classField.get(me));
                 //BA.Log("Saving: " + classField.getName() + " " + Modifier.toString(classField.getModifiers()) + " " + classField.getType().getSimpleName());
               } catch (IllegalArgumentException e) {
                 BA.Log("Failed saving: " + classField.getName() + " " + Modifier.toString(classField.getModifiers()) + " " + classField.getType().getSimpleName());
                 //e.printStackTrace();
               } catch (IllegalAccessException e) {
                 BA.Log("Failed saving: " + classField.getName() + " " + Modifier.toString(classField.getModifiers()) + " " + classField.getType().getSimpleName());
                 //e.printStackTrace();
               }
             }
           }
         
         }
       }
     
     }
   }

B4X:
public void RestoreSessionVariables(B4AClass me, HttpSessionWrapper session) {
     //pageFieldName = "_" + pageFieldName.toLowerCase();
     Field[] fields = me.getClass().getDeclaredFields();
     for (Field classField : fields) {     
       try {
         if (classField!=null) {
           switch (classField.getType().getSimpleName().toLowerCase()) {
           case "websocket":
           case "abmpage":
           case "abmaterial":
           case "common":
             break;
           default:
             String packageName = BA.packageName.toLowerCase();
             //BA.Log("PackageName: " + packageName);
             switch (classField.getName().toLowerCase()) {
             case "htsubs":
               break;
             default:
               if (classField.getType().getName().toLowerCase().startsWith(packageName)) {
                 try {
                   @SuppressWarnings("rawtypes")
                   Class c = Class.forName(classField.getType().getName());
                   boolean HasHtSubs=false;
                   try {
                     c.getDeclaredField("htSubs");
                     HasHtSubs=true;
                   } catch (NoSuchFieldException nsfe) {
                    // intentionally ignored                   
                   }
                   if (HasHtSubs) {
                     //BA.Log("Loading: " + classField.getName());
                     boolean accesible = classField.isAccessible();
                     classField.setAccessible(true);
               
                     classField.set(me, session.GetAttribute(classField.getName()));
                     classField.setAccessible(accesible);
                   }
                 } catch (ClassNotFoundException e1) {
                   e1.printStackTrace();
                 }
               
               } else {
                 //BA.Log("Loading: " + classField.getName());
                 boolean accesible = classField.isAccessible();
                 classField.setAccessible(true);
           
                 classField.set(me, session.GetAttribute(classField.getName()));
                 classField.setAccessible(accesible);
               }
             }
           }         
         }
       } catch (IllegalArgumentException e) {
         BA.Log("Failed restoring: " + classField.getName() + " " + Modifier.toString(classField.getModifiers()) + " " + classField.getType().getSimpleName());
         //e.printStackTrace();
       
       } catch (IllegalAccessException e) {
         BA.Log("Failed restoring: " + classField.getName() + " " + Modifier.toString(classField.getModifiers()) + " " + classField.getType().getSimpleName());
         //e.printStackTrace();
       
       }
     }
   }
 
Last edited:
Upvote 0

alwaysbusy

Expert
Licensed User
Longtime User
But doing this you will keep a reference of that websocket class in session so it will not be disposed. So if the user's internet connection never enstablishes the server will hold a reference of that websocket class untill the session will expire.
I do see with Wireshark that the Websocket is closed when the user disconnects. Websocket_Disconnected is also called. (the websocket itself is not in the session and on the page I've disconnected the reference (hopefully).

B4X:
Private Sub WebSocket_Disconnected
   Log("Disconnected")
   page.ws = Null
   ' And all other variables you need for your program
   page.SaveSessionVariables(Me, ws.session)
End Sub

This really is a brain teaser especially because it is so difficult to monitor.
 
Upvote 0

mindful

Active Member
Licensed User
@PCastagnetti
It seems that adding a filter that will monitor "/ws/*" is NOT a good ideea after all, because this is an upgrade request so we need to set the session cookie before the upgrade request.

There are three problems that I found regarding Safari's problem with session cookie:
1. We need to have a session created before the upgrade request
2. Safari has a problem with localhost cookies (but i think this is not you case because you are connection from the ipad to the b4j server)
3. Safari has a problem with cookies that don't have the path set, so I checked the session cookie path that comes out of the server and is null, on Chrome it sets to "/" but I don't know if Safari does this also (I can't test because I don't have an Apple device)

So if we get rid of all this problems, maybe, just maybe we do not have to get the session at each request for a page but we need to get it once in the correct form - so we can use the filter (ABMSessionCreator) that monitors "/js/b4j_ws.min.js" which will be called only one time, just before the browser caches the file.

You can test and see if this works for you:
1. Replace the code from the Filter Sub in ABMSessionCreator with this one:
B4X:
'if there is no cookie stored in browser we need to create one and set the cookie path to "/" also mark it as HttpOnly
If req.GetCookies.Length = 0 Then
  Dim joCookie as JavaObject
  joCookie.InitializeNewInstance("javax.servlet.http.Cookie", Array("JSESSIONID", req.GetSession.Id))
  joCookie.RunMethod("setPath", Array("/"))
  joCookie.RunMethod("setHttpOnly", Array(True))
  'joCookie.RunMethod("setDomain", Array("www.yourdomain.com"))
  resp.AddCookie(joCookie)
  Log("Cookie created for session " & req.GetSession.Id)
End If
Return True
2. in ABMApplication, in StartServer you should have the ABMSessionCreator filter setup like this (like in the DemoDynamic):
B4X:
srvr.AddFilter("/js/b4j_ws.min.js", "ABMSessionCreator", False)

You may have notice that one line from the Filter sub is commented. Try like this and see if it works, maybe is not needed to set the cookie domain!

Clear the cache from browser and access you application.

To see if the you get if have any cookies stored in the browser which are passed to the server you need to put a Log in WebSocket_Connected sub of every page:
B4X:
Log("Your server session is " & ws.Session.Id)
Log("You have " & ws.UpgradeRequest.GetCookies.Length & " cookie(s)")
For i = 0 to ws.UpgradeRequest.GetCookies.Length - 1
  If ws.UpgradeRequest.GetCookies(i).Name = "JSESSIONID" Then
    Log("Your browser session is " & ws.UpgradeRequest.GetCookies(i).Value)
    Exit
  End If
Next

Please try this and post your result ...

Later Edit: you need to reference JavaObject library in your project.
 
Upvote 0

PCastagnetti

Member
Licensed User
Longtime User
Today I have only an iPhone on which I did a little test:
The behavior is the same compared to the iPad (variable session = null )
I tried the @mindful code , but unfortunately the problem still occurs.

For now I stay with the code (#41) which seems to solve the problem.
Unfortunately today I can not do other tests ...


thanks to @mindful
 
Upvote 0

mindful

Active Member
Licensed User
@PCastagnetti ... the code from post #41 - " ws.Eval("location.reload(arguments[0])",ArrayAs Object(True)) " will invalidate your current cache and reload the page and needed files (css, js, images, etc) from the server, therefore will make a request for b4j_ws.min which will make make the Filter sub in ABMSessionCreator fire. But doing like this you won't get cache for your files ... of course of only for iOS.

So I finally figured it out how to cache only some files and make b4j_ws.min.js NOT cacheable .. this is ok because it's a very small file.

To do this:
1. Create a new filter class - CacheFilter and in its Filter sub you add:
B4X:
Public Sub Filter(req As ServletRequest, resp As ServletResponse) As Boolean
    ' Test if the request is for a static file, only static files will be cached.
    If File.Exists(File.Combine(File.DirApp, "www"), req.RequestURI) Then
        If File.GetName(req.RequestURI) = "b4j_ws.min.js" Then ' we need to disable the cache for the b4j_ws.min.js file
            resp.SetHeader("Cache-Control", "no-cache, no-store") ' HTTP 1.1 disable cache
            resp.SetHeader("Pragma", "no-cache") ' HTTP 1.0 disable cache
            resp.SetHeader("Expires", "0") ' Proxies disable cache
            Log("Sent DO NOT Cache headers for : " & req.RequestURI)
            Return True
        End If
        resp.SetHeader("Cache-Control", "public, max-age=604800") ' enable the cache with a lifetime of 7 days for all static files
        Log("Sent Cache headers for : " & req.RequestURI)
    End If
    Return True
End Sub
2. Add the new filter in ABMApplication in StartServer:
B4X:
srvr.AddFilter("/*", "CacheFilter", False)
3. Modify the lines before srvr.Start like this:
B4X:
    #If RELEASE
        srvr.SetStaticFilesOptions(CreateMap("gzip":True,"dirAllowed":False))
    #Else
        srvr.SetStaticFilesOptions(CreateMap("gzip":False,"dirAllowed":False))
    #End If

Now because the b4j_ws.min.js isn't cacheable the Filter sub in ABMSessionCreator will fire at every request for that file.

So this way you have cache for all the files except the b4j_ws.min.js on all your clients browsers.

Just tested and it works with iPhone 7 and iPad 2.

Later edit: Don't forget to clear the browser cache before you try this!

Using this code you could exclude other fles from beeing cached, I had this need as I generate some simple static html files on runtime (custom error pages) and those pages where cached and the browser always showed the cached version.

Happy programming
 
Last edited:
Upvote 0

PCastagnetti

Member
Licensed User
Longtime User
your code works ...
I go a little off topic:
by changing the post (#42) code I can send an error message to the user in the case of cookies off in your browser, or any reason that modifies the session id.
when you connect I do a check

B4X:
Dim cIP As String = ws.UpgradeRequest.RemoteAddress
    If Not(ABMShared.IsValidIp(cIP))  Then
        ErrMsg= "ip address error"
       
    Else
        Dim cTry As Int=Main.MapSession.GetDefault(cIP,1)
        If cTry <=3 And Not(ws.Session.HasAttribute("Refresh")) Then
           
            Main.MapSession.Put(cIP,cTry+1)
            ws.Eval("location.reload(arguments[0])",Array As Object(True))           
            ws.Session.SetAttribute("Refresh",True)
            Log(ws.Session.Id)
            Return
        Else
            If cTry>3 Then
               
                ErrMsg="Disabled cookies"
            End If
            If Main.MapSession.ContainsKey(cIP) Then Main.MapSession.Remove(cIP)
        End If
    End If
 
Upvote 0

mindful

Active Member
Licensed User
@PCastagnetti you can check if a browser has cookies enabled using this code which uses the navigator object:
B4X:
    Dim cookieCheck As Future = ws.EvalWithResult("return navigator.cookieEnabled;", Null)
    Log(cookieCheck.Value)

Or if you do not want to rely on the navigator object (ie browser sometimes returns true if cookies are disabled) then you can do it like this:
B4X:
    Dim jsScript As String = $"var hasCookies = ("cookie" in document && (document.cookie.length > 0 || (document.cookie = "testCookie").indexOf.call(document.cookie, "testCookie") > -1)); return hasCookies;"$
    Dim cookieCheck As Future = ws.EvalWithResult(jsScript, Null)
    Log(cookieCheck.Value)

Your code consumes resources ... Think if you have many concurent ipads you will run this code and refresh the browser cache every time ...
 
Upvote 0

mindful

Active Member
Licensed User
@alwaysbusy just a heads up regarding SaveSessionVariables and RestoreSessionVariables methods you posted: They use the session attributes. If a users opens more that one tab in their browser (and does different stuff in each one) all the tabs from one browser share the same session therefore the same attributes and using the methods from above will work only if one tab is open per session...
 
Upvote 0
Cookies are required to use this site. You must accept them to continue using the site. Learn more…