Overview

Currently we post to /api/preview/{entity_type}/{entity} when the model changes and we need to udpate the preview.
This saves to the auto-save without discrimination.

This means that last user who triggers an auto-save can wipe out the previous work of other users. They can also wipe out their previous work if they happen to have an open tab editing the same entity in XB that they left open days ago.

Example

  1. User 1 opens page 1 in XB, makes 1 edit, leave tab open
  2. User 2 opens page 1 in XB after user 1, starts off with user 1's edit
  3. User 2 spends 2 days editing page 1
  4. User 2 closes the XB tab in their browser
  5. User 1 comes back to page 1 in XB that left open 2 days ago
  6. User 1 makes another edit to page 1 in the existing tab
  7. RESULT: All of user 2's changes will be wiped out and will be unrecoverable. Neither User 1 or User 2 will be given a notice that this happened.

Another example could be when user 1 and 2 are trying to edit the page at the same time. Everytime 1 user makes a change and triggers an auto-save it wipe the changes the other user just made.

Proposed resolution

  1. Have \Drupal\experience_builder\Controller\ApiLayoutController::get(), \Drupal\experience_builder\Controller\ApiLayoutController::patch and \Drupal\experience_builder\Controller\ApiLayoutController::post include a unique UUI for each auto-save in their response, for the main entity and all regions.
  2. The client will return this when calling \Drupal\experience_builder\Controller\ApiLayoutController::patch(). and \Drupal\experience_builder\Controller\ApiLayoutController::post. If the auto-save hashes do not match the match the current auto-save hashes return an error code to the client.

While not perfect it will prevent any user from losing more that there edit in XB. Because we trigger an auto-save for each edit they will not be able do more than 1 edit before they find out they can't edit. If another user is not currently editing the same entity, for instance if the current user just left a tab open for an hour or a few days, refreshing the browser tab would allow to continue editing.

Remaining questions

  1. How should we handle a situation where the main entity auto-save hash matches but 1 or more region auto-save hashes do not match? How should handle the reverse situation where main entity does match but all the regions do.
  2. How should the client react to this new error? We could either tell the user to refresh page or refresh the page for them.
    As a first pass in this issue we could just mention in error from server that the user should refresh the page to keep editing, then in a follow determine if the client should do more

Future enhancements

Not in this issue but in the future once #3492065: Enable concurrent editing: Replace the postPreview action with atomic equivalents is done we can look into....

For each item in the model, keep a version number, initialize this to 1 when we first write to the auto-save entry for that entity.
With all patchModelAndLayout and patchComponent operations, check the value sent by the client against the currently stored value.
If they don't match, return a HTTP Conflict error.
If they do, increment the version when updating the value.
Work out how to do something similar for the layout structure.

User interface changes

If another user has triggered an auto-save since the current user has made changes then the current user will get an error

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

larowlan created an issue. See original summary.

larowlan’s picture

wim leers’s picture

Assigned: Unassigned » larowlan
Issue tags: +Needs issue summary update

… which AFAICT would not need everything written up in the issue summary — because an even more rudimentary implementation is now possible.

larowlan’s picture

The auto save hashes are at the whole entity level. But we need them at the component level

tedbow’s picture

Issue summary: View changes

re #4 and #5
I chatted with @lauriii, @effulgentsia and @balintbrews about this on yesterday about this.

The decision was that as first pass we should only check that the user has the latest version of auto-save, probably by comparing that last known auto-save.

so re #5

The auto save hashes are at the whole entity level. But we need them at the component level

I agree this is not an as ideal as what you have suggested, it still would allow us to add more advanced conflict detection later.

tedbow’s picture

tedbow’s picture

Assigned: larowlan » tedbow

working on this

tedbow’s picture

Status: Active » Needs work

Right now in the MR I have only worked on getting the 3 \Drupal\Tests\experience_builder\Kernel\ApiLayoutController*Test classes to pass

a bunch of other things should break because they are not sending the new 'autoSaves' key back to the server. Probably all of the e2e tests should fail

tedbow’s picture

This is still very rough. I will pick it back up tomorrow

tedbow’s picture

I am wondering if there will be any security concerns with sending the actual hash of the auto-save to the client.

Right now I don't think there are because we pretty much send the contents of the auto save to the client via \Drupal\experience_builder\Controller\ApiLayoutController::get anyways, so having the hash is not giving any more information.

but after #3525829: If user with less/different permissions edits a page after a user with more/different permissions data loss could occur we may store fields in the auto-save that user does not view or edit access to. We will then probably fitler what's in the auto-save before we send it to the client.

In that case imagine this scenario

  1. the only field the user does not have access to is a boolean field.
  2. the user knows the name of the field
  3. the knowledge from the source code the hash is actually of whole auto-save including the field they don't have access to.

Would it be possible for the user to reconstruct the complete auto-save data from the information they have andgenerate the has themselves, 1 time with the boolean field FALSE and 1 time with the boolean field TRUE and then just check which one matches the hash they were sent?
I know it might be an edge case but this could also work with any field that has limited number of possible values

My other thought is to just send a auto-save version number.

Initially in this issue @larowlan suggested

For each item in the model, keep a version number, initialize this to 1 when we first write to the auto-save entry for that entity.

I am not sure he was considering this or he just suggested using a version number because of security concerns or just he was referring to sub-items of the auto-save we don't have hashes already for.

tedbow’s picture

To address #12 I am just going to use UUID that will get saved with auto-save each time.

I didn't want to use an auto-incremented number because a malicious user could just guess the current ones to purposefully wipe of another users changes and save there's.

tedbow’s picture

Issue summary: View changes
tedbow’s picture

re #12 and #13 I actually realized that modules/contrib/experience_builder/src/Controller/ApiAutoSaveController.php:107 already returns hash of auto-save entries so this is not new problem.

So I have reverted back to using the hash

tedbow’s picture

Assigned: tedbow » Unassigned

I think this could probably use some other eyes on before I work more on it.

So far it is the back-end implementation I mentioned in #6.

I gave a first pass(with AI help at the front). The front-end changes work with the caveat I mention in the MR about rapid fire requests.
The last test that is failing for me locally is entity-form-field-types-test.cy.js when the test loop through many different fields, so I am pretty sure this is because of the same problem. I could probably get the tests passing with the correct waiting on requests. I have fixed a few test this way in this issue already.

tedbow’s picture

Status: Needs work » Needs review
tedbow’s picture

Issue tags: +Needs followup

so I don't forget

tedbow’s picture

Issue tags: -Needs followup

To address @larowlan's feedback I have made this a backend only issue and follow-up handle the Frontend work. I create #3526907: For content entities add client support conflict detection via the `autoSaves' key as follow-up

In that issue I pointed to the commits that remove the /ui if the person tackling that wants to start from my work

wim leers’s picture

Assigned: Unassigned » larowlan
Issue tags: -stable blocker +beta blocker

Didn't @lauriii consider this a beta blocker?

Discussed with @effulgentsia, and we both agree @larowlan is the ideal reviewer for this.

larowlan’s picture

Assigned: larowlan » Unassigned
Status: Needs review » Reviewed & tested by the community

Addressed my own review and will merge if this comes back green

larowlan’s picture

  • larowlan committed 285035b0 on 0.x authored by tedbow
    Issue #3490565 by tedbow, larowlan, wim leers: Add rudimentary conflict...
larowlan’s picture

Status: Reviewed & tested by the community » Fixed

Merged to 0.x - thanks!

wim leers’s picture

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.