Introduce an edit confirmation checks/locking REST API

Hi everyone,

I would like to introduce a JavaScript helper for handling the pre-edit checks (checking if the page is locked, if it is part of an extension, if the page would be broken by editing due to missing script/… rights, etc.). These pre-edit checks can either produce errors (that prevent editing) or warnings (that require an explicit user confirmation).

We already have a similar implementation for in-place editing, but it is tied to this specific edit mode and not re-usable. Further, the server-side endpoint is a wiki page (XWiki.InplaceEditing) which is far from ideal for re-usability. The idea is to extract what we currently have for in-place editing and make it reusable (also for Cristal).

For this reason, I propose introducing a REST API that handles the server-side part of the pre-edit checks and optional document locking.

Before discussing the actual API design, I would like to discuss a few design considerations:

Locking Behavior

Locking should be optional, as we already don’t check if the page is locked when using the realtime editor. Further, when editing only, say, a boolean property, it makes little sense to use locking – but we should still execute the remaining checks. The decision if the page shall be locked currently depends on the editor (like “inline”, or “wysiwyg”). However, this concept of “editor” makes little sense in the context of the REST API, as it is not tied to a specific frontend implementation and the server cannot know about every possible editor implementation.

For this reason, I think the client should be responsible for deciding if locking is required or not.
A question here is: should we still check for locks when not asking for locking?

I don’t think so – if the client does not ask for locking, we should not check for locks either. To implement this, I suggest augmenting the edit confirmation checker Java API to allow passing a list of checks to skip. The REST API wouldn’t expose this directly, but it would use this feature to skip the lock check when not asking for locking.

Forcing Checks

At the moment, whenever the user confirms, all checks are “forced”, which means that an identifier is stored in the session to avoid showing the same check again. I think this might not always be desirable, as it means that if a user skips the warning and then wants to go back to see it again, they will not see it. I think it would be better to add an explicit “Remember confirmation” option to the API so the UI is free to add an option for users to choose whether they want to remember the confirmation or not. I think we didn’t do this back when we introduced the confirmations because we feared that further requests during the editing process would show the same confirmation again. But it could be a possibility in a new UI like Cristal where we could more clearly separate the confirmation from the actual editing process.

Further, I noticed that there is a possible gap in the current implementation that if the document is changed while the user is confirming, the warnings the user sees might not be the same as the ones they confirm. For this reason, I suggest introducing a confirmation token that is basically a hash of the shown confirmation messages. This token would be transmitted together with the warnings to the client and would need to be provided back to the server when the user confirms. The server would then re-compute the token based on the current version of the document and compare it with the one provided by the client. If they match, the confirmation is valid and the confirmations can be forced.

The lock would, of course, still always be forced if the client asks for locking and provides a valid confirmation token.

Output Format of Warning Messages

Internally, the edit confirmation checker API returns Block elements, but they frequently already
just contain HTML content, including complex UI elements like expandable sections for required rights. For this reason, we have no real choice but to return HTML content for the error and warning messages. I would still propose returning them as individual messages in the response so the client can decide how to display them, in particular, if there are several warnings/errors.

Proposed REST endpoints

The same API exists for the default version of a page and for translations, similar to other page endpoints in the XWiki REST API.

  • Default translation:

    POST /wikis/{wikiName}/spaces/{spaceName: .+}/pages/{pageName}/editconfirmation

  • Explicit translation:

    POST /wikis/{wikiName}/spaces/{spaceName: .+}/pages/{pageName}/translations/{language}/editconfirmation

Request

All fields are optional unless stated otherwise.

  • lock: boolean

    • true: attempt to lock for editing (only done if there are no warnings/errors or the user has confirmed the warnings)
    • false: unlock (no checks are run)
    • omitted: only run pre-edit checks
  • confirmationToken: String

    An opaque token returned by a previous response when confirmation is required. When present, it indicates that the user has confirmed and that the request may proceed (including lock override, if needed).

  • rememberConfirmation: boolean

    Only meaningful when confirmationToken is provided. When true, the server will persist the last confirmed warnings in the session (so that the exact same warnings are not shown again for this document in the same session).

Response

  • warnings: List<String> - HTML content for each warning message, if any
  • errors: List<String> - HTML content for each error message, if any
  • errorMessage: String - an optional additional error message in case the requested action cannot be performed, e.g., because the page has been modified in the meantime and the confirmation token is no longer valid. This error message isn’t related to the pre-edit checks and doesn’t require confirmation.
  • confirmationToken: String - returned when confirmation is required (and can be used in the next request)

Status codes

  • 200 OK

    Request processed successfully and no (further) confirmation is required.

  • 409 Conflict

    Confirmation is required (warnings and/or errors exist) or the provided confirmationToken is not valid anymore. In both cases, the response body contains the confirmation details to display.

  • 401 Unauthorized

    Authentication is required or the current user lacks view/edit rights for the page.

Processing rules

  1. If lock is omitted, run pre-edit checks and return them if confirmation is required.
  2. If lock = false, unlock and return 200.
  3. If lock = true:
    • If no confirmationToken is provided:
      • Run pre-edit checks.
      • If warnings/errors exist, return 409 with confirmationRequired=true and a confirmationToken.
      • If no warnings/errors exist, lock and return 200.
    • If confirmationToken is provided:
      • Re-run the pre-edit checks.
      • If the token is still valid or no warnings/errors exist:
        • Lock the document. If it was locked by another user, override the lock.
        • If rememberConfirmation=true, persist the confirmed warnings in the session.
        • Return 200.
      • Otherwise, return 409 with a fresh confirmation payload.

Example flow

1) Attempt to lock before editing

Request:

POST /wikis/xwiki/spaces/Main/pages/WebHome/editconfirmation
Content-Type: application/json

{ "lock": true }

Response (confirmation required):

409 Conflict
Content-Type: application/json

{
  "warnings": ["<div>... rendered HTML warnings (including lock warning if any) ...</div>"],
  "errors": [],
  "errorMessage": null,
  "confirmationToken": "<opaque>"
}

2) User confirms

Request (confirm, override lock if needed, and remember the decision):

POST /wikis/xwiki/spaces/Main/pages/WebHome/editconfirmation
Content-Type: application/json

{
  "lock": true,
  "confirmationToken": "<opaque>",
  "rememberConfirmation": true
}

Response:

200 OK
Content-Type: application/json

{
  "warnings": [],
  "errors": [],
  "errorMessage": null,
  "confirmationToken": "<opaque>"
}

3) Unlock after editing

POST /wikis/xwiki/spaces/Main/pages/WebHome/editconfirmation
Content-Type: application/json

{ "lock": false }
200 OK
Content-Type: application/json

{
  "warnings": [],
  "errors": [],
  "errorMessage": null,
  "confirmationToken": null
}

Choices Regarding the API Design

Separate Locking and Pre-Edit Checks

From an API design, it seems pretty weird to have a single endpoint that handles both locking and pre-edit checks. However, it also seems not so nice to clients if these two operations are split into two different endpoints as they would always need to be used together. If locking was a separate endpoint, we would also either still need to include the lock warning in the pre-edit checks (which seems weird) or clients would basically need to re-implement the lock warning separately, which goes against the idea of the generic edit confirmation checkers.

More Information in the Response

We could also consider adding more information to the response, such as the actual lock status of the document, the user who currently has the lock, etc. We could also include metadata for every check. At the moment, while we have the information about the locks, it feels weird to include this information in the pre-edit checks response. For the other checks, we currently don’t have any metadata, and it’s also not clear to me how clients would use this information.

Non-HTML Response Format

At the moment, the response contains only HTML content which makes them hard or even impossible to use in non-web contexts. It would require a significant effort to provide non-HTML representations of the warnings and errors, but it could be interesting for clients that aren’t web applications.

Using HTTP Headers for Confirmation Token

Instead of including the confirmation token in the response body, we could use the ETag HTTP header to transmit the confirmation token and then use an If-Match header to transmit the token back to the server when confirming. The response code should then be 412 Precondition Failed if the token is not valid anymore.

It is not fully clear to me if this would be a better API design, in particular as we’re not using this in other places.

Use DELETE for Unlocking

Instead of using a POST request with lock=false, we could use a DELETE request to unlock the document. This would be more consistent with the HTTP method semantics.

Further Confirmation Tokens

We could consider using edit confirmation tokens more broadly, like during document saving to check if there have been new warnings/errors since the last confirmation. We would need to make sure that the token is still valid after confirming the warnings.

I’m looking forward to your feedback on this proposal and any suggestions for improvements.

1 Like

Seems good to me, just a few things:

  • Creating a look should be a PUT method without a boolean parameter, to stay consistent with HTTP methods
  • Warnings and errors should not be message strings, they should be enumeration variants that can then be translated by the client
  • I’m against using HTTP headers to transmit the token, as it is part of the information. It should provided as a parameter in the request’s body

EDIT: Could be interesting to use standardized JSON (https://jsonapi.org/) for the response

Hi, thanks for your proposal.

Overall, I’m +1 for it, my only concern is that you only talk about a new endpoint (/editconfirmation), which makes sense for XS but I feel like the locked status should also be directly checked on PUT requests, when editing the page (it could be through an optional withConfirmation parameter).

The problem is that for example for required rights checks, the warning contains an expandable section that lists all macros, XObjects, … that would either break or be newly executed due to the rights change. This is quite hard to express as an enumeration variant. Also, extension can provide additional edit confirmation checks so whatever list of possible values we provide will never be exhaustive. I’m not sure an enumeration variant thus provides a lot of value if the possible values cannot be listed exhaustively. What we could try is to have an additional (translated) plain text value that at least contains the most important information.

So you say we should have a dedicated lock endpoint? I’m asking because, as I’ve mentioned in the proposal, I think it should be possible to get all this information without actually locking the page.

On the one hand, it’s good point but on the other hand, it’s not how we handle this in the XWiki UI:

  • we check locks before the user starts editing, not during saving.
  • during saving, we instead check if the page has been modified since the start of the editing (independent of the lock status) by passing a version parameter. When the version doesn’t match the current version, we perform a three-way merge. If the merge doesn’t succeed without conflicts, the user gets a conflict dialog.

I’m not sure if it makes sense to provide a lock check during saving in the REST API, what would make more sense to me is to have the version check with optional merge in the REST API, too.

In XWiki, we use JAXB to model objects for the REST API which are then serialized to XML or JSON. I don’t know how this would work with standardized JSON. I would try as much as possible to stay consistent with what we already have here. If you think we should change the REST API to follow this standard, maybe you could provide a separate proposal with an analysis how our current REST API differs from this standard and what we would need to change to follow it. If these changes aren’t too breaking, maybe we could implement at least some of them to get closer to the standard.

1 Like

We skip the edit lock check only if there is an existing realtime editing session. But I agree that the client should decide whether to rely on edit locking or not.

Looks good overall, so +1 on my side.

Thanks,
Marius