ICEfaces
  1. ICEfaces
  2. ICE-8149

When using Push across separate portlets the ViewState values are incorrect

    Details

      Description

      Initially reported by Neil at Liferay (http://issues.liferay.com/browse/FACES-1171), there appears to be a problem when using Ajax Push as an inter-portlet communication (IPC) strategy. This is where one portlet changes some shared state and attempts to render all the relevant views.

      The symptom is a SessionExpiredException reported by our custom exception handler.

        Activity

        Hide
        Deryk Sinotte added a comment -

        Here are my notes on the issue to date:

        1) The issue seems constrained to a problem with Push. I can run and interact with two separate portlets without seeing the issue. The exception only occurs after one or two Push renders are triggered.

        2) The SessionExpiredException is slightly misleading. We add that exception handling to attempt to catch cases where a ViewExpiredException is thrown in a situation when the session has expired. In this case, it's not a session expiry but truly a ViewExpiredException.

        3) I've narrowed it down to the fact to an update that comes back and updates the javax.faces.ViewState so that the returned value is applied to all views on the page (not just the portlet it actually belongs to. I've also constructed a test case that illustrates the ViewState "contamination" that occurs. In other words, when interacting with the first portlet and triggering a push, the updates for the 2nd portlet also include a ViewState value that gets applied back to the 1st portlet. Any subsequent interaction with that first portlet will throw a ViewExpiredException but that ViewState value doesn't exist for that view.

        Show
        Deryk Sinotte added a comment - Here are my notes on the issue to date: 1) The issue seems constrained to a problem with Push. I can run and interact with two separate portlets without seeing the issue. The exception only occurs after one or two Push renders are triggered. 2) The SessionExpiredException is slightly misleading. We add that exception handling to attempt to catch cases where a ViewExpiredException is thrown in a situation when the session has expired. In this case, it's not a session expiry but truly a ViewExpiredException. 3) I've narrowed it down to the fact to an update that comes back and updates the javax.faces.ViewState so that the returned value is applied to all views on the page (not just the portlet it actually belongs to. I've also constructed a test case that illustrates the ViewState "contamination" that occurs. In other words, when interacting with the first portlet and triggering a push, the updates for the 2nd portlet also include a ViewState value that gets applied back to the 1st portlet. Any subsequent interaction with that first portlet will throw a ViewExpiredException but that ViewState value doesn't exist for that view.
        Hide
        Neil Griffin added a comment -

        Thanks for looking into this Deryk.

        Show
        Neil Griffin added a comment - Thanks for looking into this Deryk.
        Hide
        Deryk Sinotte added a comment -

        Using the Chrome console and two simple test portlets, you can see the issue happening. After initial load of the portal page but before any interaction, we query for all the ViewStates. Since there are two portlets and 3 forms per portlet (1 that is from the portlet markup and two hidden ones that ICEfaces inserts), it returns 6 values, 3 for each portlet:

        document.getElementsByName('javax.faces.ViewState');
        [
        <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"-3978125222353783743:​1573083955214999241" autocomplete=​"off">​
        ,
        <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"-3978125222353783743:​1573083955214999241" autocomplete=​"off">​
        ,
        <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"-3978125222353783743:​1573083955214999241" autocomplete=​"off">​
        ,
        <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"7928353047340332639:​-1008142066134303525" autocomplete=​"off">​
        ,
        <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"7928353047340332639:​-1008142066134303525" autocomplete=​"off">​
        ,
        <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"7928353047340332639:​-1008142066134303525" autocomplete=​"off">​
        ]

        After clicking the "Update with Push" button, the console logs which updates are being applied:

        ...
        [window] applied updates >>
        ..
        update["javax.faces.ViewState"]: -3978125222353783743:1573083955214999241.... location3:1230
        [window] applied updates >>
        ...
        update["javax.faces.ViewState"]: -3978125222353783743:1573083955214999241.... location3:1230
        [window] applied updates >>
        ...
        update["javax.faces.ViewState"]: 7928353047340332639:-1008142066134303525.... location3:1230

        Querying again for the ViewState values, you can see that all of the forms now have the ViewState of the second portlet:

        document.getElementsByName('javax.faces.ViewState');
        [
        <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"7928353047340332639:​-1008142066134303525" autocomplete=​"off">​
        ,
        <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"7928353047340332639:​-1008142066134303525" autocomplete=​"off">​
        ,
        <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"7928353047340332639:​-1008142066134303525" autocomplete=​"off">​
        ,
        <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"7928353047340332639:​-1008142066134303525" autocomplete=​"off" style>​
        ,
        <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"7928353047340332639:​-1008142066134303525" autocomplete=​"off" style>​
        ,
        <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"7928353047340332639:​-1008142066134303525" autocomplete=​"off" style>​

        Show
        Deryk Sinotte added a comment - Using the Chrome console and two simple test portlets, you can see the issue happening. After initial load of the portal page but before any interaction, we query for all the ViewStates. Since there are two portlets and 3 forms per portlet (1 that is from the portlet markup and two hidden ones that ICEfaces inserts), it returns 6 values, 3 for each portlet: document.getElementsByName('javax.faces.ViewState'); [ <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"-3978125222353783743:​1573083955214999241" autocomplete=​"off">​ , <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"-3978125222353783743:​1573083955214999241" autocomplete=​"off">​ , <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"-3978125222353783743:​1573083955214999241" autocomplete=​"off">​ , <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"7928353047340332639:​-1008142066134303525" autocomplete=​"off">​ , <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"7928353047340332639:​-1008142066134303525" autocomplete=​"off">​ , <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"7928353047340332639:​-1008142066134303525" autocomplete=​"off">​ ] After clicking the "Update with Push" button, the console logs which updates are being applied: ... [window] applied updates >> .. update ["javax.faces.ViewState"] : -3978125222353783743:1573083955214999241.... location3:1230 [window] applied updates >> ... update ["javax.faces.ViewState"] : -3978125222353783743:1573083955214999241.... location3:1230 [window] applied updates >> ... update ["javax.faces.ViewState"] : 7928353047340332639:-1008142066134303525.... location3:1230 Querying again for the ViewState values, you can see that all of the forms now have the ViewState of the second portlet: document.getElementsByName('javax.faces.ViewState'); [ <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"7928353047340332639:​-1008142066134303525" autocomplete=​"off">​ , <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"7928353047340332639:​-1008142066134303525" autocomplete=​"off">​ , <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"7928353047340332639:​-1008142066134303525" autocomplete=​"off">​ , <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"7928353047340332639:​-1008142066134303525" autocomplete=​"off" style>​ , <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"7928353047340332639:​-1008142066134303525" autocomplete=​"off" style>​ , <input type=​"hidden" name=​"javax.faces.ViewState" id=​"javax.faces.ViewState" value=​"7928353047340332639:​-1008142066134303525" autocomplete=​"off" style>​
        Hide
        Deryk Sinotte added a comment -

        If I read the logging correctly, it may be applying 3 sets up updates (one client-initiated and 2-push initiated) each time the button in the test case is clicked (triggering a render):

        The first update looks like a push response where both portlets are getting updated with the first portlet's ViewState:

        [window] applied updates >>
        update["_jpfcpncuivr_A4157_j_id1:porFrm1:_t6"]: <span id="_jpfcpncuivr_A4157_j_id1:porFr....
        update["_jpfcpncuivr_A4157_j_id1:porFrm1:_t10"]: <span id="_jpfcpncuivr_A4157_j_id1:porFr....
        update["dynamic-code-compat"]: <span id="dynamic-code-compat"></span>....
        update["javax.faces.ViewState"]: -1592934675537635146:-618886728610481250.... location3:1230

        The next update appears to be a response to the button click and only updates the first portlet with it's proper ViewState:

        [window] applied updates >>
        update["_jpfcpncuivr_A4157_j_id1:porFrm1:_t6"]: <span id="_jpfcpncuivr_A4157_j_id1:porFr....
        update["javax.faces.ViewState"]: -1592934675537635146:-618886728610481250.... location3:1230

        Then the third response is another push response that updates both portlets with the second portlet's ViewState, leaving is in an undesirable state on the client:

        [window] applied updates >>
        update["_jpfcpncuivr_A3977_j_id1:porFrm2:_t6"]: <span id="_jpfcpncuivr_A3977_j_id1:porFr....
        update["_jpfcpncuivr_A3977_j_id1:porFrm2:_t10"]: <span id="_jpfcpncuivr_A3977_j_id1:porFr....
        update["dynamic-code-compat"]: <span id="dynamic-code-compat"></span>....
        update["javax.faces.ViewState"]: -3747725226717534306:-454402066662664180....

        Show
        Deryk Sinotte added a comment - If I read the logging correctly, it may be applying 3 sets up updates (one client-initiated and 2-push initiated) each time the button in the test case is clicked (triggering a render): The first update looks like a push response where both portlets are getting updated with the first portlet's ViewState: [window] applied updates >> update ["_jpfcpncuivr_A4157_j_id1:porFrm1:_t6"] : <span id="_jpfcpncuivr_A4157_j_id1:porFr.... update ["_jpfcpncuivr_A4157_j_id1:porFrm1:_t10"] : <span id="_jpfcpncuivr_A4157_j_id1:porFr.... update ["dynamic-code-compat"] : <span id="dynamic-code-compat"></span>.... update ["javax.faces.ViewState"] : -1592934675537635146:-618886728610481250.... location3:1230 The next update appears to be a response to the button click and only updates the first portlet with it's proper ViewState: [window] applied updates >> update ["_jpfcpncuivr_A4157_j_id1:porFrm1:_t6"] : <span id="_jpfcpncuivr_A4157_j_id1:porFr.... update ["javax.faces.ViewState"] : -1592934675537635146:-618886728610481250.... location3:1230 Then the third response is another push response that updates both portlets with the second portlet's ViewState, leaving is in an undesirable state on the client: [window] applied updates >> update ["_jpfcpncuivr_A3977_j_id1:porFrm2:_t6"] : <span id="_jpfcpncuivr_A3977_j_id1:porFr.... update ["_jpfcpncuivr_A3977_j_id1:porFrm2:_t10"] : <span id="_jpfcpncuivr_A3977_j_id1:porFr.... update ["dynamic-code-compat"] : <span id="dynamic-code-compat"></span>.... update ["javax.faces.ViewState"] : -3747725226717534306:-454402066662664180....
        Hide
        Deryk Sinotte added a comment -

        Since you can successfully use the push-enabled button on the second portlet multiple times, this would further indicate that the problem is ViewState updates are not being confined to the portlet/view to which they belong.

        Show
        Deryk Sinotte added a comment - Since you can successfully use the push-enabled button on the second portlet multiple times, this would further indicate that the problem is ViewState updates are not being confined to the portlet/view to which they belong.
        Hide
        Deryk Sinotte added a comment -

        The issue is in our core application.js file and is related to changes in ICE-7188. The section of code in question is:

        //MyFaces uses a linked list of view state keys to track the changes in the view state – the participating
        //forms need to have their view state key updated so that the next submit will work with the latest saved state
        //ICE-7188

        var formViewID;

        namespace.onBeforeSubmit(function(source)

        { formViewID = formOf(source)['ice.view'].value; console.warn("BEFORE SUBMIT: form id = " + formViewID); }

        );

        namespace.onAfterUpdate(function(updates) {
        ifViewStateUpdated(updates, function(viewState) {
        //update only the forms that have the same viewID with the one used by the submitting form
        each(document.getElementsByTagName('form'), function(form) {
        var viewIDElement = form['ice.view'];
        var viewStateElement = form['javax.faces.ViewState'];
        if (viewStateElement && viewIDElement && viewIDElement.value == formViewID)

        { viewStateElement.value = viewState; console.warn("AFTER UPDATE: form id = " + formViewID + " -> " + viewState); }

        });
        });
        });

        What happens in the above two sections is that, before a submit is done, the view id of the form that initiated the request is stored. Then after the updates are applied, we make an attempt to ensure the all the forms in the view that originally made the submission are updated with the new ViewState values. However, in the case there are multiple views (portlets) on the pages and Push is used, there are additional requests made by Push but the form id is never updated. So the last ViewState update that arrives due to a Push response will potentially update all the forms on the page because the original form id is still referenced.

        Show
        Deryk Sinotte added a comment - The issue is in our core application.js file and is related to changes in ICE-7188 . The section of code in question is: //MyFaces uses a linked list of view state keys to track the changes in the view state – the participating //forms need to have their view state key updated so that the next submit will work with the latest saved state // ICE-7188 var formViewID; namespace.onBeforeSubmit(function(source) { formViewID = formOf(source)['ice.view'].value; console.warn("BEFORE SUBMIT: form id = " + formViewID); } ); namespace.onAfterUpdate(function(updates) { ifViewStateUpdated(updates, function(viewState) { //update only the forms that have the same viewID with the one used by the submitting form each(document.getElementsByTagName('form'), function(form) { var viewIDElement = form ['ice.view'] ; var viewStateElement = form ['javax.faces.ViewState'] ; if (viewStateElement && viewIDElement && viewIDElement.value == formViewID) { viewStateElement.value = viewState; console.warn("AFTER UPDATE: form id = " + formViewID + " -> " + viewState); } }); }); }); What happens in the above two sections is that, before a submit is done, the view id of the form that initiated the request is stored. Then after the updates are applied, we make an attempt to ensure the all the forms in the view that originally made the submission are updated with the new ViewState values. However, in the case there are multiple views (portlets) on the pages and Push is used, there are additional requests made by Push but the form id is never updated. So the last ViewState update that arrives due to a Push response will potentially update all the forms on the page because the original form id is still referenced.
        Hide
        Deryk Sinotte added a comment -

        The responsible change was r26345 done for ICE-7465.

        case 'begin':
        //trigger notification only when submit is user-initiated
        if (!source || source.id != retrieveUpdateFormID(viewIDOf(source)))

        { broadcast(perRequestOnBeforeSubmitListeners, [ source ]); }

        break;

        In that case, a test was put in to prevent onBeforeSubmit listeners from being called if the request was triggered by something other than the client request (ie push). This was to prevent the status indicators from firing unless the request came from a purposeful, client interaction. Unfortunately, bluntly turning off all onBeforeSubmit callbacks causes the problem we see here.

        Show
        Deryk Sinotte added a comment - The responsible change was r26345 done for ICE-7465 . case 'begin': //trigger notification only when submit is user-initiated if (!source || source.id != retrieveUpdateFormID(viewIDOf(source))) { broadcast(perRequestOnBeforeSubmitListeners, [ source ]); } break; In that case, a test was put in to prevent onBeforeSubmit listeners from being called if the request was triggered by something other than the client request (ie push). This was to prevent the status indicators from firing unless the request came from a purposeful, client interaction. Unfortunately, bluntly turning off all onBeforeSubmit callbacks causes the problem we see here.
        Hide
        Deryk Sinotte added a comment -

        Checked in some changes to prevent the updated ViewState being applied to forms outside the relevant portlet that initiated the request or push updates.

        Show
        Deryk Sinotte added a comment - Checked in some changes to prevent the updated ViewState being applied to forms outside the relevant portlet that initiated the request or push updates.
        Hide
        Neil Griffin added a comment -

        Thanks Deryk!

        Show
        Neil Griffin added a comment - Thanks Deryk!
        Hide
        Ken Fyten added a comment -

        This fix should be applied to the maintenance branch for EE 3.0.0.GA_P01.

        Show
        Ken Fyten added a comment - This fix should be applied to the maintenance branch for EE 3.0.0.GA_P01.
        Hide
        Deryk Sinotte added a comment -

        Changes applied to the maintenance branch:

        Sending compat/core/src/main/javascript/status.js
        Sending core/src/main/javascript/application.js
        Sending core/src/main/javascript/blockui.js

        Committed revision 29265.

        Show
        Deryk Sinotte added a comment - Changes applied to the maintenance branch: Sending compat/core/src/main/javascript/status.js Sending core/src/main/javascript/application.js Sending core/src/main/javascript/blockui.js Committed revision 29265.
        Hide
        Deryk Sinotte added a comment -

        Attaching simple test case that uses push across portlets on the same page.

        Show
        Deryk Sinotte added a comment - Attaching simple test case that uses push across portlets on the same page.

          People

          • Assignee:
            Deryk Sinotte
            Reporter:
            Deryk Sinotte
          • Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

            Dates

            • Created:
              Updated:
              Resolved: