CVE-2024-21648 (published 2024-01-09T00:15Z)

XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. The rollback action is missing a right protection, a user can rollback to a previous version of the page to gain rights they don't have anymore. The problem has been patched in XWiki 14.10.17, 15.5.3 and 15.8-rc-1 by ensuring that the rights are checked before performing the rollback. 

CVSS v3 base score: 8.8, HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-274
cwe_name: Improper Handling of Insufficient Privileges
description: The product does not handle or incorrectly handles when it has insufficient privileges to perform an operation, leading to resultant weaknesses.
extended_description: 
            
            
            
            
         
         

Fix information:
repo_url: https://github.com/xwiki/xwiki-platform
commit_details: repo_url: https://github.com/xwiki/xwiki-platform
author: Simon Urli
author_date: 2023-08-30 17:08:16+02:00
msg: XWIKI-21257: Rollback is not triggering a UserUpdatingDocumentEvent

  * Ensure that the rollback API properly calls the check method
  * Back the changes in a unit test that check events triggered in
    rollback
repository: repo_url: https://github.com/xwiki/xwiki-platform
repo_name: xwiki/xwiki-platform
description: The XWiki platform
repo_language: Java
owner: xwiki
file_changes: filename: CustomUserUpdatedDocumentEventListener.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,74 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.test;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.xwiki.component.annotation.Component;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.observation.AbstractEventListener;
+import org.xwiki.observation.event.Event;
+
+import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.internal.event.UserUpdatingDocumentEvent;
+
+/**
+ * Listener dedicated to cancel a specific rolling back event triggered in VersionIT test.
+ *
+ * @version $Id$
+ * @since 14.10.17
+ * @since 15.5.3
+ * @since 15.8RC1
+ */
+@Component
+@Singleton
+@Named(CustomUserUpdatedDocumentEventListener.NAME)
+public class CustomUserUpdatedDocumentEventListener extends AbstractEventListener
+{
+    static final String NAME = "CustomUserUpdatedDocumentEventListener";
+
+    @Inject
+    private Logger logger;
+
+    /**
+     * Default constructor.
+     */
+    public CustomUserUpdatedDocumentEventListener()
+    {
+        super(NAME, new UserUpdatingDocumentEvent());
+    }
+
+    @Override
+    public void onEvent(Event event, Object source, Object data)
+    {
+        UserUpdatingDocumentEvent userEvent = (UserUpdatingDocumentEvent) event;
+        XWikiDocument sourceDoc = (XWikiDocument) source;
+        DocumentReference expectedReference = new DocumentReference("xwiki", "XWiki", "XWikiPreferences");
+        if (StringUtils.equals(userEvent.getUserReference().getName(), "DeleteVersionTestUserCancelEvent")
+            && sourceDoc.getDocumentReference().equals(expectedReference)) {
+            logger.info("Cancelling user event on purpose");
+            userEvent.cancel();
+        }
+    }
+}

programming_language: Java
filename: components.txt
change_type: ModificationType.MODIFY
diff: @@ -1 +1,2 @@
+org.xwiki.test.CustomUserUpdatedDocumentEventListener
 org.xwiki.test.TestMacro

programming_language: SQL
filename: VersionIT.java
change_type: ModificationType.MODIFY
diff: @@ -19,6 +19,8 @@
  */
 package org.xwiki.flamingo.test.docker;
 
+import java.util.List;
+
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Order;
 import org.junit.jupiter.api.Test;
@@ -26,17 +28,21 @@
 import org.xwiki.flamingo.skin.test.po.AttachmentsPane;
 import org.xwiki.flamingo.skin.test.po.AttachmentsViewPage;
 import org.xwiki.model.reference.AttachmentReference;
+import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.rest.model.jaxb.Page;
 import org.xwiki.test.docker.junit5.TestReference;
 import org.xwiki.test.docker.junit5.UITest;
 import org.xwiki.test.ui.TestUtils;
 import org.xwiki.test.ui.po.HistoryPane;
 import org.xwiki.test.ui.po.ViewPage;
+import org.xwiki.test.ui.po.editor.ObjectEditPage;
+import org.xwiki.test.ui.po.editor.ObjectEditPane;
 import org.xwiki.test.ui.po.editor.WikiEditPage;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.startsWith;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
@@ -321,4 +327,322 @@ void testDeleteAllButFirstVersion(TestUtils setup, TestReference testReference)
         assertEquals("1.1", page.getVersion());
         assertEquals("1.1", page.getContent());
     }
+
+    /**
+     * Scenario:
+     *   * Create a user RollbackTestUser
+     *   * Create a page, allow RollbackTestUser script right on it, and then deny it
+     *   * Login with RollbackTestUser and try to rollback the page to the version where the right was allowed
+     *   * Check that the page still has the right xobject set to deny
+     */
+    @Test
+    @Order(7)
+    void testRollbackDontMessUpRights(TestUtils setup, TestReference testReference) throws Exception
+    {
+        setup.loginAsSuperAdmin();
+        setup.rest().delete(testReference);
+        String rollbackTestUser = "RollbackTestUser";
+        setup.rest().delete(new DocumentReference("xwiki", "XWiki", rollbackTestUser));
+        setup.createUser(rollbackTestUser, rollbackTestUser, "");
+        setup.createPage(testReference, "Test Rollback Page");
+
+        setup.setRights(testReference, "", "XWiki." + rollbackTestUser, "script", true);
+
+        // We don't use setRights twice as it would create another object and we want to edit the existing one.
+        setup.gotoPage(testReference, "edit", "editor=object");
+        ObjectEditPage objectEditPage = new ObjectEditPage();
+        List<ObjectEditPane> rightObjects = objectEditPage.getObjectsOfClass("XWiki.XWikiRights", true);
+        assertEquals(1, rightObjects.size());
+        ObjectEditPane objectEditPane = rightObjects.get(0);
+        assertEquals("XWiki." + rollbackTestUser, objectEditPane.getFieldValue(objectEditPane.byPropertyName("users")));
+        assertEquals("script", objectEditPane.getFieldValue(objectEditPane.byPropertyName("levels")));
+        assertEquals("1", objectEditPane.getFieldValue(objectEditPane.byPropertyName("allow")));
+        objectEditPane.setFieldValue(objectEditPane.byPropertyName("allow"), "0");
+        // We want a minor version
+        objectEditPage.clickSaveAndContinue();
+        setup.gotoPage(testReference);
+
+        setup.login(rollbackTestUser, rollbackTestUser);
+
+        // check that the right is as expected
+        setup.gotoPage(testReference, "edit", "editor=object");
+        objectEditPage = new ObjectEditPage();
+        rightObjects = objectEditPage.getObjectsOfClass("XWiki.XWikiRights", true);
+        assertEquals(1, rightObjects.size());
+        objectEditPane = rightObjects.get(0);
+        assertEquals("XWiki." + rollbackTestUser, objectEditPane.getFieldValue(objectEditPane.byPropertyName("users")));
+        assertEquals("script", objectEditPane.getFieldValue(objectEditPane.byPropertyName("levels")));
+        assertEquals("0", objectEditPane.getFieldValue(objectEditPane.byPropertyName("allow")));
+
+        ViewPage viewPage = objectEditPage.clickCancel();
+        HistoryPane historyPane = viewPage.openHistoryDocExtraPane();
+        historyPane = historyPane.showMinorEdits();
+
+        // Check that the history contains what we're expecting
+        assertEquals(3, historyPane.getNumberOfVersions());
+        assertEquals("2.2", historyPane.getCurrentVersion());
+        assertTrue(historyPane.hasVersion("2.1"));
+
+        viewPage = historyPane.rollbackToVersion("2.1");
+        historyPane = viewPage.openHistoryDocExtraPane();
+        historyPane = historyPane.showMinorEdits();
+
+        // Check that the history contains what we're expecting
+        assertEquals(4, historyPane.getNumberOfVersions());
+
+        assertEquals("3.1", historyPane.getCurrentVersion());
+        assertEquals("Rollback to version 2.1", historyPane.getCurrentVersionComment());
+        assertTrue(historyPane.hasVersion("2.2"));
+        assertTrue(historyPane.hasVersion("2.1"));
+
+        // check that the right is still the same
+        setup.gotoPage(testReference, "edit", "editor=object");
+        objectEditPage = new ObjectEditPage();
+        rightObjects = objectEditPage.getObjectsOfClass("XWiki.XWikiRights", true);
+        assertEquals(1, rightObjects.size());
+        objectEditPane = rightObjects.get(0);
+        assertEquals("XWiki." + rollbackTestUser, objectEditPane.getFieldValue(objectEditPane.byPropertyName("users")));
+        assertEquals("script", objectEditPane.getFieldValue(objectEditPane.byPropertyName("levels")));
+        assertEquals("0", objectEditPane.getFieldValue(objectEditPane.byPropertyName("allow")));
+
+        objectEditPage.clickCancel();
+    }
+
+    /**
+     * Scenario:
+     *   * Create a user DeleteVersionTestUser
+     *   * Give DeleteVersionTestUser Admin right with a dedicated xobject in XWiki.XWikiPreferences
+     *   * Give DeleteVersionTestUser PR right with a dedicated xobject in XWiki.XWikiPreferences
+     *   * Edit the xobject to deny PR right to DeleteVersionTestUser
+     *   * Login with DeleteVersionTestUser and delete last version of XWiki.XWikiPreferences
+     *   * Check that the version has been deleted but the PR right is still denied
+     */
+    @Test
+    @Order(8)
+    void testDeleteVersionDontMessUpRights(TestUtils setup) throws Exception
+    {
+        setup.loginAsSuperAdmin();
+
+        String deleteVersionTestUser = "DeleteVersionTestUser";
+        setup.rest().delete(new DocumentReference("xwiki", "XWiki", deleteVersionTestUser));
+        setup.createUser(deleteVersionTestUser, deleteVersionTestUser, "");
+
+        setup.setGlobalRights("", deleteVersionTestUser, "admin", true);
+
+        DocumentReference xwikiPreferences = new DocumentReference("xwiki", "XWiki", "XWikiPreferences");
+        setup.gotoPage(xwikiPreferences, "view", "viewer=history");
+        HistoryPane historyPane = new HistoryPane();
+        historyPane = historyPane.showMinorEdits();
+        // store the current version as it will be our basis for next steps
+        String latestVersionBeforeChanges = historyPane.getCurrentVersion();
+        int numberOfVersions = historyPane.getNumberOfVersions();
+
+        // We just create a new major version in the history
+        setup.gotoPage(xwikiPreferences, "edit", "editor=wiki");
+        WikiEditPage wikiEditPage = new WikiEditPage();
+        wikiEditPage.clickSaveAndView();
+
+        setup.gotoPage(xwikiPreferences, "view", "viewer=history");
+        historyPane = new HistoryPane();
+        // Version where we start our changes
+        String startChangesVersion = historyPane.getCurrentVersion();
+
+        String currentMajor = startChangesVersion.split("\\.")[0];
+
+        setup.setGlobalRights("", deleteVersionTestUser, "programming", true);
+        currentMajor = String.valueOf(Integer.parseInt(currentMajor) + 1);
+
+        // We don't use setRights twice as it would create another object and we want to edit the existing one.
+        setup.gotoPage(xwikiPreferences, "edit", "editor=object");
+        ObjectEditPage objectEditPage = new ObjectEditPage();
+        List<ObjectEditPane> rightObjects = objectEditPage.getObjectsOfClass("XWiki.XWikiGlobalRights", false);
+        ObjectEditPane rightObject = rightObjects.get(rightObjects.size() - 1);
+        rightObject.displayObject();
+        assertEquals(deleteVersionTestUser, rightObject.getFieldValue(rightObject.byPropertyName("users")));
+        assertEquals("programming", rightObject.getFieldValue(rightObject.byPropertyName("levels")));
+        assertEquals("1", rightObject.getFieldValue(rightObject.byPropertyName("allow")));
+
+        rightObject.setFieldValue(rightObject.byPropertyName("allow"), "0");
+        // We want a minor version
+        objectEditPage.clickSaveAndContinue();
+
+        setup.gotoPage(xwikiPreferences);
+
+        setup.login(deleteVersionTestUser, deleteVersionTestUser);
+
+        // first check that the right is properly denied in the objects
+        setup.gotoPage(xwikiPreferences, "edit", "editor=object");
+        objectEditPage = new ObjectEditPage();
+        rightObjects = objectEditPage.getObjectsOfClass("XWiki.XWikiGlobalRights", false);
+        rightObject = rightObjects.get(rightObjects.size() - 1);
+        rightObject.displayObject();
+        assertEquals(deleteVersionTestUser, rightObject.getFieldValue(rightObject.byPropertyName("users")));
+        assertEquals("programming", rightObject.getFieldValue(rightObject.byPropertyName("levels")));
+        assertEquals("0", rightObject.getFieldValue(rightObject.byPropertyName("allow")));
+
+        setup.gotoPage(xwikiPreferences, "view", "viewer=history");
+        historyPane = new HistoryPane();
+        historyPane = historyPane.showMinorEdits();
+        assertEquals(numberOfVersions + 3, historyPane.getNumberOfVersions());
+        assertEquals(currentMajor + ".2", historyPane.getCurrentVersion());
+        assertTrue(historyPane.hasVersion(currentMajor + ".1"));
+
+        try {
+            historyPane = historyPane.deleteVersion(historyPane.getCurrentVersion());
+            historyPane = historyPane.showMinorEdits();
+            assertEquals(numberOfVersions + 2, historyPane.getNumberOfVersions());
+            assertEquals(currentMajor + ".1", historyPane.getCurrentVersion());
+
+            // Check that the page remained with rights unchanged
+            setup.gotoPage(xwikiPreferences, "edit", "editor=object");
+            objectEditPage = new ObjectEditPage();
+            rightObjects = objectEditPage.getObjectsOfClass("XWiki.XWikiGlobalRights", false);
+            rightObject = rightObjects.get(rightObjects.size() - 1);
+            rightObject.displayObject();
+            assertEquals(deleteVersionTestUser,
+                rightObject.getFieldValue(rightObject.byPropertyName("users")));
+            assertEquals("programming", rightObject.getFieldValue(rightObject.byPropertyName("levels")));
+            assertEquals("0", rightObject.getFieldValue(rightObject.byPropertyName("allow")));
+            objectEditPage.clickCancel();
+        } finally {
+            // Put back the page in the state it was before our changes
+            setup.loginAsSuperAdmin();
+            setup.gotoPage(xwikiPreferences, "view", "viewer=history");
+            historyPane = new HistoryPane();
+            historyPane = historyPane.showMinorEdits();
+            historyPane.rollbackToVersion(latestVersionBeforeChanges);
+        }
+    }
+
+    /**
+     * Scenario:
+     *   * Same as above but using DeleteVersionTestUserCancelEvent as user to trigger the
+     *   {@link org.xwiki.test.CustomUserUpdatedDocumentEventListener} which should cancel immediately the change
+     *   * Expectation here is that the reset is not performed at all
+     */
+    @Test
+    @Order(9)
+    void testDeleteVersionDontMessUpRightsWithCancellingEvent(TestUtils setup)
+        throws Exception
+    {
+        setup.loginAsSuperAdmin();
+
+        String deleteVersionTestUser = "DeleteVersionTestUserCancelEvent";
+        setup.rest().delete(new DocumentReference("xwiki", "XWiki", deleteVersionTestUser));
+        setup.createUser(deleteVersionTestUser, deleteVersionTestUser, "");
+
+        setup.setGlobalRights("", deleteVersionTestUser, "admin", true);
+
+        DocumentReference xwikiPreferences = new DocumentReference("xwiki", "XWiki", "XWikiPreferences");
+        setup.gotoPage(xwikiPreferences, "view", "viewer=history");
+        HistoryPane historyPane = new HistoryPane();
+        historyPane = historyPane.showMinorEdits();
+        // store the current version as it will be our basis for next steps
+        String latestVersionBeforeChanges = historyPane.getCurrentVersion();
+        int numberOfVersions = historyPane.getNumberOfVersions();
+
+        // We just create a new major version in the history
+        setup.gotoPage(xwikiPreferences, "edit", "editor=wiki");
+        WikiEditPage wikiEditPage = new WikiEditPage();
+        wikiEditPage.clickSaveAndView();
+
+        setup.gotoPage(xwikiPreferences, "view", "viewer=history");
+        historyPane = new HistoryPane();
+        // Version where we start our changes
+        String startChangesVersion = historyPane.getCurrentVersion();
+
+        String currentMajor = startChangesVersion.split("\\.")[0];
+
+        setup.setGlobalRights("", deleteVersionTestUser, "programming", true);
+        currentMajor = String.valueOf(Integer.parseInt(currentMajor) + 1);
+
+        // We don't use setRights twice as it would create another object and we want to edit the existing one.
+        setup.gotoPage(xwikiPreferences, "edit", "editor=object");
+        ObjectEditPage objectEditPage = new ObjectEditPage();
+        List<ObjectEditPane> rightObjects = objectEditPage.getObjectsOfClass("XWiki.XWikiGlobalRights", false);
+        ObjectEditPane rightObject = rightObjects.get(rightObjects.size() - 1);
+        rightObject.displayObject();
+        assertEquals(deleteVersionTestUser, rightObject.getFieldValue(rightObject.byPropertyName("users")));
+        assertEquals("programming", rightObject.getFieldValue(rightObject.byPropertyName("levels")));
+        assertEquals("1", rightObject.getFieldValue(rightObject.byPropertyName("allow")));
+
+        rightObject.setFieldValue(rightObject.byPropertyName("allow"), "0");
+        // We want a minor version
+        objectEditPage.clickSaveAndContinue();
+
+        setup.gotoPage(xwikiPreferences);
+
+        setup.login(deleteVersionTestUser, deleteVersionTestUser);
+
+        // first check that the right is properly denied in the objects
+        setup.gotoPage(xwikiPreferences, "edit", "editor=object");
+        objectEditPage = new ObjectEditPage();
+        rightObjects = objectEditPage.getObjectsOfClass("XWiki.XWikiGlobalRights", false);
+        rightObject = rightObjects.get(rightObjects.size() - 1);
+        rightObject.displayObject();
+        assertEquals(deleteVersionTestUser, rightObject.getFieldValue(rightObject.byPropertyName("users")));
+        assertEquals("programming", rightObject.getFieldValue(rightObject.byPropertyName("levels")));
+        assertEquals("0", rightObject.getFieldValue(rightObject.byPropertyName("allow")));
+
+        setup.gotoPage(xwikiPreferences, "view", "viewer=history");
+        historyPane = new HistoryPane();
+        historyPane = historyPane.showMinorEdits();
+        assertEquals(numberOfVersions + 3, historyPane.getNumberOfVersions());
+        assertEquals(currentMajor + ".2", historyPane.getCurrentVersion());
+        assertTrue(historyPane.hasVersion(currentMajor + ".1"));
+
+        try {
+            historyPane.deleteVersion(historyPane.getCurrentVersion());
+
+            setup.gotoPage(xwikiPreferences, "view", "viewer=history");
+            historyPane = new HistoryPane();
+            historyPane = historyPane.showMinorEdits();
+
+            // here the history shouldn't have changed because of the CustomUserUpdatedDocumentEventListener
+            // that should have cancel the event
+            assertEquals(numberOfVersions + 3, historyPane.getNumberOfVersions());
+            assertEquals(currentMajor + ".2", historyPane.getCurrentVersion());
+
+            // Check that the page remained with rights unchanged
+            setup.gotoPage(xwikiPreferences, "edit", "editor=object");
+            objectEditPage = new ObjectEditPage();
+            rightObjects = objectEditPage.getObjectsOfClass("XWiki.XWikiGlobalRights", false);
+            rightObject = rightObjects.get(rightObjects.size() - 1);
+            rightObject.displayObject();
+            assertEquals(deleteVersionTestUser, rightObject.getFieldValue(rightObject.byPropertyName("users")));
+            assertEquals("programming", rightObject.getFieldValue(rightObject.byPropertyName("levels")));
+            assertEquals("0", rightObject.getFieldValue(rightObject.byPropertyName("allow")));
+            objectEditPage.clickCancel();
+
+            setup.gotoPage(xwikiPreferences, "view", "viewer=history");
+            historyPane = new HistoryPane();
+            historyPane = historyPane.showMinorEdits();
+
+            // Check that deleting another version still works
+            historyPane = historyPane.deleteVersion(currentMajor + ".1");
+            historyPane = historyPane.showMinorEdits();
+
+            assertEquals(numberOfVersions + 2, historyPane.getNumberOfVersions());
+            assertEquals(currentMajor + ".2", historyPane.getCurrentVersion());
+            assertFalse(historyPane.hasVersion(currentMajor + ".1"));
+
+            // Check that the page remained with rights unchanged
+            setup.gotoPage(xwikiPreferences, "edit", "editor=object");
+            objectEditPage = new ObjectEditPage();
+            rightObjects = objectEditPage.getObjectsOfClass("XWiki.XWikiGlobalRights", false);
+            rightObject = rightObjects.get(rightObjects.size() - 1);
+            rightObject.displayObject();
+            assertEquals(deleteVersionTestUser, rightObject.getFieldValue(rightObject.byPropertyName("users")));
+            assertEquals("programming", rightObject.getFieldValue(rightObject.byPropertyName("levels")));
+            assertEquals("0", rightObject.getFieldValue(rightObject.byPropertyName("allow")));
+            objectEditPage.clickCancel();
+        } finally {
+            // Put back the page in the state it was before our changes
+            setup.loginAsSuperAdmin();
+            setup.gotoPage(xwikiPreferences, "view", "viewer=history");
+            historyPane = new HistoryPane();
+            historyPane = historyPane.showMinorEdits();
+            historyPane.rollbackToVersion(latestVersionBeforeChanges);
+        }
+    }
 }

programming_language: Java
filename: XWiki.java
change_type: ModificationType.MODIFY
diff: @@ -4708,6 +4708,27 @@ public void checkDeletingDocument(DocumentReference userReference, XWikiDocument
      */
     public void deleteDocumentVersions(XWikiDocument document, String version1, String version2, XWikiContext context)
         throws XWikiException
+    {
+        deleteDocumentVersions(document, version1, version2, false, context);
+    }
+
+    /**
+     * Delete a range of versions from a document history.
+     * 
+     * @param document the document from which to delete versions
+     * @param version1 one end of the versions range to remove
+     * @param version2 the other end of the versions range to remove
+     * @param triggeredByUser {@code true} if the API is called directly by an action from a user and checks need to
+     * be performed for the rollback (See: {@link #rollback(XWikiDocument, String, boolean, boolean, XWikiContext)}).
+     * @param context the XWiki context
+     * @throws XWikiException
+     * @since 14.10.17
+     * @since 15.5.3
+     * @since 15.8RC1
+     */
+    @Unstable
+    public void deleteDocumentVersions(XWikiDocument document, String version1, String version2,
+        boolean triggeredByUser, XWikiContext context) throws XWikiException
     {
         Version v1 = new Version(version1);
         Version v2 = new Version(version2);
@@ -4750,20 +4771,22 @@ public void deleteDocumentVersions(XWikiDocument document, String version1, Stri
                 .notify(new DocumentVersionRangeDeletingEvent(document.getDocumentReferenceWithLocale(),
                     lowerBound.toString(), upperBound.toString()), document, context);
 
-            // Update the archive
-            context.getWiki().getVersioningStore().saveXWikiDocArchive(archive, true, context);
-            // Make sure the cached document archive is updated too
-            XWikiDocument cachedDocument =
-                context.getWiki().getDocument(document.getDocumentReferenceWithLocale(), context);
-            cachedDocument.setDocumentArchive(archive);
 
             // There are still some versions left.
             // If we delete the most recent (current) version, then rollback to latest undeleted version.
+            // We do that right before updating the archive, in case it would cancel the action.
             Version previousVersion = archive.getLatestVersion();
             if (!document.getRCSVersion().equals(previousVersion)) {
-                context.getWiki().rollback(document, previousVersion.toString(), false, context);
+                context.getWiki().rollback(document, previousVersion.toString(), false, triggeredByUser, context);
             }
 
+            // Update the archive
+            context.getWiki().getVersioningStore().saveXWikiDocArchive(archive, true, context);
+            // Make sure the cached document archive is updated too
+            XWikiDocument cachedDocument =
+                context.getWiki().getDocument(document.getDocumentReferenceWithLocale(), context);
+            cachedDocument.setDocumentArchive(archive);
+
             // Notify after versions delete
             getObservationManager()
                 .notify(new DocumentVersionRangeDeletedEvent(document.getDocumentReferenceWithLocale(),
@@ -7569,6 +7592,25 @@ private void restoreDeletedAttachment(XWikiAttachment rolledbackAttachment, XWik
      */
     public XWikiDocument rollback(final XWikiDocument tdoc, String rev, boolean addRevision, XWikiContext xcontext)
         throws XWikiException
+    {
+        return rollback(tdoc, rev, addRevision, false, xcontext);
+    }
+
+    /**
+     * @param tdoc the document to rollback
+     * @param rev the revision to rollback to
+     * @param addRevision true if a new revision should be created
+     * @param triggeredByUser {@code true} if this has been triggered by a user and a check needs to be performed
+     * @param xcontext the XWiki context
+     * @return the new document
+     * @throws XWikiException when failing to rollback the document
+     * @since 14.10.17
+     * @since 15.5.3
+     * @since 15.8RC1
+     */
+    @Unstable
+    public XWikiDocument rollback(final XWikiDocument tdoc, String rev, boolean addRevision,
+        boolean triggeredByUser, XWikiContext xcontext) throws XWikiException
     {
         LOGGER.debug("Rolling back [{}] to version [{}]", tdoc, rev);
 
@@ -7647,6 +7689,10 @@ public XWikiDocument rollback(final XWikiDocument tdoc, String rev, boolean addR
             message = localizePlainOrKey("core.comment.rollback", rev);
         }
 
+        if (triggeredByUser) {
+            checkSavingDocument(xcontext.getUserReference(), document, message, false, xcontext);
+        }
+
         ObservationManager om = getObservationManager();
         if (om != null) {
             // Notify listeners about the document that is going to be rolled back.

programming_language: Java
filename: DeleteVersionsAction.java
change_type: ModificationType.MODIFY
diff: @@ -65,7 +65,7 @@ public boolean action(XWikiContext context) throws XWikiException
         Version v2 = versions[1];
 
         if (v1 != null && v2 != null) {
-            context.getWiki().deleteDocumentVersions(tdoc, v1.toString(), v2.toString(), context);
+            context.getWiki().deleteDocumentVersions(tdoc, v1.toString(), v2.toString(), true, context);
         }
 
         sendRedirect(context);

programming_language: Java
filename: RollbackAction.java
change_type: ModificationType.MODIFY
diff: @@ -79,7 +79,7 @@ public boolean action(XWikiContext context) throws XWikiException
         }
 
         // Perform the rollback.
-        xwiki.rollback(tdoc, rev, context);
+        xwiki.rollback(tdoc, rev, true, true, context);
 
         // Forward to view.
         String redirect = Utils.getRedirect("view", context);

programming_language: Java
filename: XWikiMockitoTest.java
change_type: ModificationType.MODIFY
diff: @@ -73,6 +73,7 @@
 import com.xpn.xwiki.doc.XWikiDocument;
 import com.xpn.xwiki.internal.ReadOnlyXWikiContextProvider;
 import com.xpn.xwiki.internal.debug.DebugConfiguration;
+import com.xpn.xwiki.internal.event.UserUpdatingDocumentEvent;
 import com.xpn.xwiki.internal.render.groovy.ParseGroovyFromString;
 import com.xpn.xwiki.internal.skin.InternalSkinManager;
 import com.xpn.xwiki.internal.store.StoreConfiguration;
@@ -216,7 +217,7 @@ public void copyDocumentPreservesAttachmentsVersion() throws Exception
     }
 
     /**
-     * Verify that {@link XWiki#rollback(XWikiDocument, String, XWikiContext)} fires the right events.
+     * Verify that {@link XWiki#rollback(XWikiDocument, String, boolean, boolean, XWikiContext)} fires the right events.
      */
     @Test
     public void rollbackFiresEvents() throws Exception
@@ -236,17 +237,22 @@ public void rollbackFiresEvents() throws Exception
         XWikiDocument result = mock(XWikiDocument.class);
         when(result.getDocumentReference()).thenReturn(documentReference);
 
+        DocumentReference userReference = new DocumentReference("xwiki", "XWiki", "ContextUser");
+        this.context.setUserReference(userReference);
+
         String revision = "3.5";
         when(this.documentRevisionProvider.getRevision(document, revision)).thenReturn(result);
 
         this.componentManager.registerMockComponent(ContextualLocalizationManager.class);
 
-        xwiki.rollback(document, revision, context);
+        xwiki.rollback(document, revision, true, true, context);
 
         verify(observationManager).notify(new DocumentRollingBackEvent(documentReference, revision), document, context);
         verify(observationManager).notify(new DocumentUpdatingEvent(documentReference), document, context);
         verify(observationManager).notify(new DocumentUpdatedEvent(documentReference), document, context);
         verify(observationManager).notify(new DocumentRolledBackEvent(documentReference, revision), document, context);
+        verify(observationManager).notify(new UserUpdatingDocumentEvent(userReference, documentReference),
+            document, context);
     }
 
     @Test

programming_language: Java
filename: HistoryPane.java
change_type: ModificationType.MODIFY
diff: @@ -183,4 +183,30 @@ public ComparePage compare(String fromVersion, String toVersion)
         getDriver().findElementWithoutWaiting(pane, By.xpath(".//input[@accesskey = 'c']")).click();
         return new ComparePage();
     }
+
+    /**
+     * @return the total number of versions contained in the history as returned by the live table
+     * @since 14.10.17
+     * @since 15.5.3
+     * @since 15.8RC1
+     */
+    public int getNumberOfVersions()
+    {
+        String xpath = ".//div[@class='paginationFilter' and following-sibling::div[@id='historycontent']]";
+        WebElement paginationDiv = getDriver().findElementWithoutWaiting(By.xpath(xpath));
+        return Integer.parseInt(getDriver().findElementWithoutWaiting(paginationDiv, By.className("totalResultsNo"))
+            .getText());
+    }
+
+    /**
+     * @return {@code true} if the requested version is currently displayed in the history
+     * @since 14.10.17
+     * @since 15.5.3
+     * @since 15.8RC1
+     */
+    public boolean hasVersion(String version)
+    {
+        String xpath = String.format(".//table//tr[contains(., '%s')]", version);
+        return getDriver().hasElementWithoutWaiting(pane, By.xpath(xpath));
+    }
 }

programming_language: Java