CVE-2023-41046: Missing Authorization (CWE-862)
value: XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. It is possible in XWiki to execute Velocity code without having script right by creating an XClass with a property of type "TextArea" and content type "VelocityCode" or "VelocityWiki". For the former, the syntax of the document needs to be set the `xwiki/1.0` (this syntax doesn't need to be installed). In both cases, when adding the property to an object, the Velocity code is executed regardless of the rights of the author of the property (edit right is still required, though). In both cases, the code is executed with the correct context author so no privileged APIs can be accessed. However, Velocity still grants access to otherwise inaccessible data and APIs that could allow further privilege escalation. At least for "VelocityCode", this behavior is most likely very old but only since XWiki 7.2, script right is a separate right, before that version all users were allowed to execute Velocity and thus this was expected and not a security issue. This has been patched in XWiki 14.10.10 and 15.4 RC1. Users are advised to upgrade. There are no known workarounds.
CWE description: The product does not perform an authorization check when an actor attempts to access a resource or perform an action.
CVSS v3 base score: 6.3
Published: 2023-09-01T20:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: Java
Files changed: xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/objects/classes/TextAreaClass.java, xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/objects/classes/TextAreaClassTest.java
Commit date: 2023-05-05 11:34:59 +0200
Commit message: XWIKI-20847/XWIKI-20848: Improve Velocity execution in TextAreaClass

* Add a unit test
* Use AuthorExecutor to set the correct context author
* Check for script right and restricted properties
* Fix a comment

Diff: diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/objects/classes/TextAreaClass.java b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/objects/classes/TextAreaClass.java
index ad2020fc8f765..d3d57d8989f4f 100644
--- a/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/objects/classes/TextAreaClass.java
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/main/java/com/xpn/xwiki/objects/classes/TextAreaClass.java
@@ -32,11 +32,15 @@
 import org.xwiki.edit.EditException;
 import org.xwiki.edit.Editor;
 import org.xwiki.edit.EditorManager;
 import org.xwiki.rendering.syntax.Syntax;
 import org.xwiki.rendering.syntax.SyntaxContent;
+import org.xwiki.security.authorization.AuthorExecutor;
+import org.xwiki.security.authorization.ContextualAuthorizationManager;
+import org.xwiki.security.authorization.Right;
 import org.xwiki.stability.Unstable;
+import org.xwiki.xml.XMLUtils;
 
 import com.xpn.xwiki.XWikiContext;
 import com.xpn.xwiki.doc.XWikiDocument;
 import com.xpn.xwiki.internal.velocity.VelocityEvaluator;
 import com.xpn.xwiki.objects.BaseCollection;
@@ -45,10 +49,13 @@
 import com.xpn.xwiki.objects.meta.PropertyMetaClass;
 import com.xpn.xwiki.web.Utils;
 
 public class TextAreaClass extends StringClass
 {
+    private static final String FAILED_VELOCITY_EXECUTION_WARNING =
+        "Failed to execute velocity code in text area property [{}]: [{}]";
+
     /**
      * Possible values for the editor meta property.
      * <p>
      * Indicates which editor should be used to manipulate the content of the property.
      */
@@ -435,51 +442,119 @@ public void displayView(StringBuffer buffer, String name, String prefix, BaseCol
         ContentType contentType = ContentType.getByValue(contentTypeString);
 
         if (contentType == ContentType.PURE_TEXT) {
             super.displayView(buffer, name, prefix, object, context);
         } else if (contentType == ContentType.VELOCITY_CODE) {
-            StringBuffer result = new StringBuffer();
-            super.displayView(result, name, prefix, object, context);
-            if (getObjectDocumentSyntax(object, context).equals(Syntax.XWIKI_1_0)) {
-                buffer.append(context.getWiki().parseContent(result.toString(), context));
-            } else {
-                // Don't do anything since this mode is deprecated and not supported in the new rendering.
-                buffer.append(result);
-            }
+            displayVelocityCode(buffer, name, prefix, object, context);
         } else {
-            BaseProperty property = (BaseProperty) object.safeget(name);
+            BaseProperty<?> property = (BaseProperty<?>) object.safeget(name);
             if (property != null) {
                 String content = property.toText();
                 XWikiDocument sdoc = getObjectDocument(object, context);
 
+                if (contentType == ContentType.VELOCITYWIKI) {
+                    content = maybeEvaluateContent(name, isolated, content, sdoc);
+                }
+
                 if (sdoc != null) {
-                    if (contentType == ContentType.VELOCITYWIKI) {
-                        // Start with a pass of Velocity
-                        // TODO: maybe make velocity+wiki a syntax so that getRenderedContent can directly take care
-                        // of that
-                        VelocityEvaluator velocityEvaluator = Utils.getComponent(VelocityEvaluator.class);
-                        content = velocityEvaluator.evaluateVelocityNoException(content,
+                    sdoc = ensureContentAuthorIsMetadataAuthor(sdoc);
+
+                    buffer.append(
+                        context.getDoc().getRenderedContent(content, sdoc.getSyntax(), isRestricted(), sdoc,
+                            isolated, context));
+                } else {
+                    buffer.append(XMLUtils.escapeElementText(content));
+                }
+            }
+        }
+    }
+
+    private static XWikiDocument ensureContentAuthorIsMetadataAuthor(XWikiDocument sdoc)
+    {
+        XWikiDocument result;
+
+        // Make sure the right author is used to execute the textarea
+        // Clone the document to avoid changing the cached document instance
+        if (!Objects.equals(sdoc.getAuthors().getEffectiveMetadataAuthor(), sdoc.getAuthors().getContentAuthor())) {
+            result = sdoc.clone();
+            result.getAuthors().setContentAuthor(sdoc.getAuthors().getEffectiveMetadataAuthor());
+        } else {
+            result = sdoc;
+        }
+
+        return result;
+    }
+
+    private String maybeEvaluateContent(String name, boolean isolated, String content, XWikiDocument sdoc)
+    {
+        if (sdoc != null) {
+            // Start with a pass of Velocity
+            // TODO: maybe make velocity+wiki a syntax so that getRenderedContent can directly take care
+            // of that
+            AuthorExecutor authorExecutor = Utils.getComponent(AuthorExecutor.class);
+            VelocityEvaluator velocityEvaluator = Utils.getComponent(VelocityEvaluator.class);
+            try {
+                return authorExecutor.call(() -> {
+                    String result;
+                    // Check script right inside the author executor as otherwise the context author might not be
+                    // correct.
+                    if (isDocumentAuthorAllowedToEvaluateScript(sdoc)) {
+                        result = velocityEvaluator.evaluateVelocityNoException(content,
                             isolated ? sdoc.getDocumentReference() : null);
+                    } else {
+                        result = content;
                     }
+                    return result;
+                }, sdoc.getAuthorReference(), sdoc.getDocumentReference());
+            } catch (Exception e) {
+                LOGGER.warn(FAILED_VELOCITY_EXECUTION_WARNING, name, ExceptionUtils.getRootCauseMessage(e));
+            }
+        }
 
-                    // Make sure the right author is used to execute the textarea
-                    // Clone the document to void messaging with the cache
-                    if (!Objects.equals(sdoc.getAuthors().getEffectiveMetadataAuthor(),
-                        sdoc.getAuthors().getContentAuthor())) {
-                        sdoc = sdoc.clone();
-                        sdoc.getAuthors().setContentAuthor(sdoc.getAuthors().getEffectiveMetadataAuthor());
-                    }
+        return content;
+    }
 
-                    buffer.append(context.getDoc().getRenderedContent(content, sdoc.getSyntax(), isRestricted(), sdoc,
-                        isolated, context));
-                } else {
-                    buffer.append(content);
-                }
+    private void displayVelocityCode(StringBuffer buffer, String name, String prefix, BaseCollection object,
+        XWikiContext context)
+    {
+        StringBuffer result = new StringBuffer();
+        super.displayView(result, name, prefix, object, context);
+        XWikiDocument sdoc = getObjectDocument(object, context);
+        if (getObjectDocumentSyntax(object, context).equals(Syntax.XWIKI_1_0) && sdoc != null) {
+            try {
+                Utils.getComponent(AuthorExecutor.class).call(() -> {
+                    // Check script right inside the author executor as otherwise the context author might not be
+                    // correct.
+                    if (isDocumentAuthorAllowedToEvaluateScript(sdoc)) {
+                        buffer.append(context.getWiki().parseContent(result.toString(), context));
+                    } else {
+                        buffer.append(result);
+                    }
+                    return null;
+                }, sdoc.getAuthorReference(), sdoc.getDocumentReference());
+            } catch (Exception e) {
+                LOGGER.warn(FAILED_VELOCITY_EXECUTION_WARNING, name, ExceptionUtils.getRootCauseMessage(e));
+                buffer.append(result);
             }
+        } else {
+            // Don't do anything since this mode is deprecated and not supported in the new rendering.
+            buffer.append(result);
         }
     }
 
+    private boolean isDocumentAuthorAllowedToEvaluateScript(XWikiDocument document)
+    {
+        boolean isAllowed = !isRestricted() && !document.isRestricted();
+
+        if (isAllowed) {
+            ContextualAuthorizationManager authorization = Utils.getComponent(ContextualAuthorizationManager.class);
+            isAllowed = authorization.hasAccess(Right.SCRIPT);
+        }
+
+        return isAllowed;
+    }
+
     private XWikiDocument getObjectDocument(BaseCollection object, XWikiContext context)
     {
         try {
             XWikiDocument doc = object.getOwnerDocument();
             if (doc == null) {
diff --git a/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/objects/classes/TextAreaClassTest.java b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/objects/classes/TextAreaClassTest.java
new file mode 100644
index 0000000000000..109a1c5432a1d
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-oldcore/src/test/java/com/xpn/xwiki/objects/classes/TextAreaClassTest.java
@@ -0,0 +1,243 @@
+/*
+ * 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 com.xpn.xwiki.objects.classes;
+
+import javax.inject.Provider;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.xwiki.model.document.DocumentAuthors;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.rendering.syntax.Syntax;
+import org.xwiki.security.authorization.Right;
+import org.xwiki.test.annotation.ComponentList;
+import org.xwiki.test.junit5.LogCaptureExtension;
+import org.xwiki.test.junit5.mockito.MockComponent;
+import org.xwiki.user.GuestUserReference;
+import org.xwiki.user.SuperAdminUserReference;
+
+import com.xpn.xwiki.XWikiContext;
+import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.internal.render.OldRendering;
+import com.xpn.xwiki.internal.security.authorization.DefaultAuthorExecutor;
+import com.xpn.xwiki.internal.velocity.VelocityEvaluator;
+import com.xpn.xwiki.objects.BaseObject;
+import com.xpn.xwiki.test.MockitoOldcore;
+import com.xpn.xwiki.test.junit5.mockito.InjectMockitoOldcore;
+import com.xpn.xwiki.test.junit5.mockito.OldcoreTest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit tests for the {@link TextAreaClass} class.
+ *
+ * @version $Id$
+ */
+@OldcoreTest
+@ComponentList({ DefaultAuthorExecutor.class })
+class TextAreaClassTest
+{
+    private static final String PROPERTY_NAME = "test";
+
+    @InjectMockitoOldcore
+    private MockitoOldcore oldcore;
+
+    @MockComponent
+    private Provider<OldRendering> oldRenderingProvider;
+
+    @MockComponent
+    private VelocityEvaluator velocityEvaluator;
+
+    @RegisterExtension
+    private final LogCaptureExtension logCaptureExtension = new LogCaptureExtension();
+
+    @Test
+    void viewWikiText()
+    {
+        // Use a spy, so we don't need to mess around with clone-support in a mock.
+        XWikiDocument spyDocument = getSpyDocument();
+
+        TextAreaClass textAreaClass = new TextAreaClass();
+        textAreaClass.setContentType(TextAreaClass.ContentType.WIKI_TEXT);
+        BaseObject object = new BaseObject();
+        object.setOwnerDocument(spyDocument);
+        object.setLargeStringValue(PROPERTY_NAME, "**Test bold**");
+        StringBuffer buffer = new StringBuffer();
+        String renderingResult = "<p><strong>Test bold</strong></p>";
+        doAnswer(invocationOnMock -> {
+            XWikiDocument sDoc = invocationOnMock.getArgument(3);
+            // Verify that the content author is set to the metadata author.
+            assertEquals(GuestUserReference.INSTANCE, sDoc.getAuthors().getContentAuthor());
+            return renderingResult;
+        }).when(spyDocument).getRenderedContent(anyString(), any(Syntax.class), anyBoolean(), any(XWikiDocument.class),
+            anyBoolean(), any(XWikiContext.class));
+        textAreaClass.displayView(buffer, PROPERTY_NAME, "", object, true, this.oldcore.getXWikiContext());
+
+        verify(spyDocument).getRenderedContent(anyString(), any(Syntax.class), anyBoolean(),
+            any(XWikiDocument.class), anyBoolean(), any(XWikiContext.class));
+
+        assertEquals(renderingResult, buffer.toString());
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = { true, false })
+    void viewVelocityCode(boolean allowExecution)
+    {
+        XWikiDocument spyDocument = getSpyDocument();
+        // Velocity code is only supported in XWiki 1.0.
+        spyDocument.setSyntax(Syntax.XWIKI_1_0);
+
+        TextAreaClass textAreaClass = new TextAreaClass();
+        textAreaClass.setContentType(TextAreaClass.ContentType.VELOCITY_CODE);
+
+        BaseObject object = new BaseObject();
+        object.setOwnerDocument(spyDocument);
+        String velocityCode = "#set($x = 1) $1 & 1";
+        object.setLargeStringValue(PROPERTY_NAME, velocityCode);
+        StringBuffer buffer = new StringBuffer();
+
+        when(this.oldcore.getMockContextualAuthorizationManager().hasAccess(Right.SCRIPT)).then(invocationOnMock -> {
+            // Verify that the content author is set to the metadata author.
+            XWikiDocument sDoc = (XWikiDocument) this.oldcore.getXWikiContext().get(XWikiDocument.CKEY_SDOC);
+            assertEquals(GuestUserReference.INSTANCE, sDoc.getAuthors().getContentAuthor());
+            return allowExecution;
+        });
+
+        String renderingResult = "1 &#38; 1";
+        OldRendering oldRendering = mock();
+        String renderingInput = velocityCode.replace("&", "&#38;");
+        when(oldRendering.parseContent(renderingInput, this.oldcore.getXWikiContext()))
+            .thenReturn(renderingResult);
+        when(this.oldRenderingProvider.get()).thenReturn(oldRendering);
+
+        textAreaClass.displayView(buffer, PROPERTY_NAME, "", object, true, this.oldcore.getXWikiContext());
+
+        if (allowExecution) {
+            assertEquals(renderingResult, buffer.toString());
+        } else {
+            assertEquals(renderingInput, buffer.toString());
+            verify(oldRendering, never()).parseContent(anyString(), any());
+        }
+
+        verify(this.oldcore.getMockContextualAuthorizationManager()).hasAccess(Right.SCRIPT);
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = { true, false })
+    void viewVelocityWiki(boolean allowExecution)
+    {
+        XWikiDocument spyDocument = getSpyDocument();
+
+        TextAreaClass textAreaClass = new TextAreaClass();
+        textAreaClass.setContentType(TextAreaClass.ContentType.VELOCITYWIKI);
+
+        BaseObject object = new BaseObject();
+        object.setOwnerDocument(spyDocument);
+        String velocityCode = "#set($x = \"bold\") **$1**";
+        object.setLargeStringValue(PROPERTY_NAME, velocityCode);
+        StringBuffer buffer = new StringBuffer();
+
+        when(this.oldcore.getMockContextualAuthorizationManager().hasAccess(Right.SCRIPT)).then(invocationOnMock -> {
+            // Verify that the content author is set to the metadata author.
+            XWikiDocument sDoc = (XWikiDocument) this.oldcore.getXWikiContext().get(XWikiDocument.CKEY_SDOC);
+            assertEquals(GuestUserReference.INSTANCE, sDoc.getAuthors().getContentAuthor());
+            return allowExecution;
+        });
+
+        String velocityResult = "**bold**";
+        when(this.velocityEvaluator.evaluateVelocityNoException(velocityCode, spyDocument.getDocumentReference()))
+            .thenReturn(velocityResult);
+
+        String renderingResult = "<p><strong>bold</strong></p>";
+
+        // Mock the rendering of the result of the Velocity code. Check that the content author is set to the
+        // metadata author.
+        doAnswer(invocationOnMock -> {
+            // Verify that the input is as expected.
+            if (allowExecution) {
+                assertEquals(velocityResult, invocationOnMock.getArgument(0));
+            } else {
+                assertEquals(velocityCode, invocationOnMock.getArgument(0));
+            }
+
+            XWikiDocument sDoc = invocationOnMock.getArgument(3);
+            assertEquals(GuestUserReference.INSTANCE, sDoc.getAuthors().getContentAuthor());
+
+            return renderingResult;
+        }).when(spyDocument).getRenderedContent(anyString(), same(Syntax.XWIKI_2_1), anyBoolean(),
+            any(XWikiDocument.class), anyBoolean(), any(XWikiContext.class));
+
+        textAreaClass.displayView(buffer, PROPERTY_NAME, "", object, true, this.oldcore.getXWikiContext());
+
+        assertEquals(renderingResult, buffer.toString());
+
+        // Verify that script right was actually checked.
+        verify(this.oldcore.getMockContextualAuthorizationManager()).hasAccess(Right.SCRIPT);
+    }
+
+    @Test
+    void viewVelocityWikiWithoutOwnerDocument()
+    {
+        TextAreaClass textAreaClass = new TextAreaClass();
+        textAreaClass.setContentType(TextAreaClass.ContentType.VELOCITYWIKI);
+
+        BaseObject object = new BaseObject();
+        String velocityCode = "1 & 2";
+        object.setLargeStringValue(PROPERTY_NAME, velocityCode);
+        StringBuffer buffer = new StringBuffer();
+
+        textAreaClass.displayView(buffer, PROPERTY_NAME, "", object, true, this.oldcore.getXWikiContext());
+
+        assertEquals("1 &#38; 2", buffer.toString());
+
+        verify(this.oldcore.getMockContextualAuthorizationManager(), never()).hasAccess(Right.SCRIPT);
+
+        assertEquals(1, this.logCaptureExtension.size());
+        assertEquals("Error while getting the syntax corresponding to object [null]."
+                + " Defaulting to using XWiki 1.0 syntax. Internal error [NullPointerException: ]",
+            this.logCaptureExtension.getMessage(0));
+    }
+
+    private XWikiDocument getSpyDocument()
+    {
+        // Use a spy, so we don't need to mess around with clone-support in a mock.
+        XWikiDocument spyDocument = spy(new XWikiDocument(new DocumentReference("wiki", "space", "page")));
+        spyDocument.setSyntax(Syntax.XWIKI_2_1);
+        this.oldcore.getXWikiContext().setDoc(spyDocument);
+        // Add some authors to the document to verify that TextAreaClass is correctly setting the content author to
+        // the metadata author.
+        DocumentAuthors authors = spyDocument.getAuthors();
+        authors.setEffectiveMetadataAuthor(GuestUserReference.INSTANCE);
+        authors.setContentAuthor(SuperAdminUserReference.INSTANCE);
+        return spyDocument;
+    }
+}

Vulnerable code:             StringBuffer result = new StringBuffer();
            super.displayView(result, name, prefix, object, context);
            if (getObjectDocumentSyntax(object, context).equals(Syntax.XWIKI_1_0)) {
                buffer.append(context.getWiki().parseContent(result.toString(), context));
            } else {
                // Don't do anything since this mode is deprecated and not supported in the new rendering.
                buffer.append(result);
            }
            BaseProperty property = (BaseProperty) object.safeget(name);
                    if (contentType == ContentType.VELOCITYWIKI) {
                        // Start with a pass of Velocity
                        // TODO: maybe make velocity+wiki a syntax so that getRenderedContent can directly take care
                        // of that
                        VelocityEvaluator velocityEvaluator = Utils.getComponent(VelocityEvaluator.class);
                        content = velocityEvaluator.evaluateVelocityNoException(content,
                    // Make sure the right author is used to execute the textarea
                    // Clone the document to void messaging with the cache
                    if (!Objects.equals(sdoc.getAuthors().getEffectiveMetadataAuthor(),
                        sdoc.getAuthors().getContentAuthor())) {
                        sdoc = sdoc.clone();
                        sdoc.getAuthors().setContentAuthor(sdoc.getAuthors().getEffectiveMetadataAuthor());
                    }
                    buffer.append(context.getDoc().getRenderedContent(content, sdoc.getSyntax(), isRestricted(), sdoc,
                        isolated, context));
                } else {
                    buffer.append(content);
                }
Fixed code: import org.xwiki.security.authorization.AuthorExecutor;
import org.xwiki.security.authorization.ContextualAuthorizationManager;
import org.xwiki.security.authorization.Right;
import org.xwiki.xml.XMLUtils;
    private static final String FAILED_VELOCITY_EXECUTION_WARNING =
        "Failed to execute velocity code in text area property [{}]: [{}]";

            displayVelocityCode(buffer, name, prefix, object, context);
            BaseProperty<?> property = (BaseProperty<?>) object.safeget(name);
                if (contentType == ContentType.VELOCITYWIKI) {
                    content = maybeEvaluateContent(name, isolated, content, sdoc);
                }

                    sdoc = ensureContentAuthorIsMetadataAuthor(sdoc);

                    buffer.append(
                        context.getDoc().getRenderedContent(content, sdoc.getSyntax(), isRestricted(), sdoc,
                            isolated, context));
                } else {
                    buffer.append(XMLUtils.escapeElementText(content));
                }
            }
        }
    }

    private static XWikiDocument ensureContentAuthorIsMetadataAuthor(XWikiDocument sdoc)
    {
        XWikiDocument result;

        // Make sure the right author is used to execute the textarea
        // Clone the document to avoid changing the cached document instance
        if (!Objects.equals(sdoc.getAuthors().getEffectiveMetadataAuthor(), sdoc.getAuthors().getContentAuthor())) {
            result = sdoc.clone();
            result.getAuthors().setContentAuthor(sdoc.getAuthors().getEffectiveMetadataAuthor());
        } else {
            result = sdoc;
        }

        return result;
    }

    private String maybeEvaluateContent(String name, boolean isolated, String content, XWikiDocument sdoc)
    {
        if (sdoc != null) {
            // Start with a pass of Velocity
            // TODO: maybe make velocity+wiki a syntax so that getRenderedContent can directly take care
            // of that
            AuthorExecutor authorExecutor = Utils.getComponent(AuthorExecutor.class);
            VelocityEvaluator velocityEvaluator = Utils.getComponent(VelocityEvaluator.class);
            try {
                return authorExecutor.call(() -> {
                    String result;
                    // Check script right inside the author executor as otherwise the context author might not be
                    // correct.
                    if (isDocumentAuthorAllowedToEvaluateScript(sdoc)) {
                        result = velocityEvaluator.evaluateVelocityNoException(content,
                    } else {
                        result = content;
                    return result;
                }, sdoc.getAuthorReference(), sdoc.getDocumentReference());
            } catch (Exception e) {
                LOGGER.warn(FAILED_VELOCITY_EXECUTION_WARNING, name, ExceptionUtils.getRootCauseMessage(e));
            }
        }
        return content;
    }
    private void displayVelocityCode(StringBuffer buffer, String name, String prefix, BaseCollection object,
        XWikiContext context)
    {
        StringBuffer result = new StringBuffer();
        super.displayView(result, name, prefix, object, context);
        XWikiDocument sdoc = getObjectDocument(object, context);
        if (getObjectDocumentSyntax(object, context).equals(Syntax.XWIKI_1_0) && sdoc != null) {
            try {
                Utils.getComponent(AuthorExecutor.class).call(() -> {
                    // Check script right inside the author executor as otherwise the context author might not be
                    // correct.
                    if (isDocumentAuthorAllowedToEvaluateScript(sdoc)) {
                        buffer.append(context.getWiki().parseContent(result.toString(), context));
                    } else {
                        buffer.append(result);
                    }
                    return null;
                }, sdoc.getAuthorReference(), sdoc.getDocumentReference());
            } catch (Exception e) {
                LOGGER.warn(FAILED_VELOCITY_EXECUTION_WARNING, name, ExceptionUtils.getRootCauseMessage(e));
                buffer.append(result);
        } else {
            // Don't do anything since this mode is deprecated and not supported in the new rendering.
            buffer.append(result);
    private boolean isDocumentAuthorAllowedToEvaluateScript(XWikiDocument document)
    {
        boolean isAllowed = !isRestricted() && !document.isRestricted();

        if (isAllowed) {
            ContextualAuthorizationManager authorization = Utils.getComponent(ContextualAuthorizationManager.class);
            isAllowed = authorization.hasAccess(Right.SCRIPT);
        }

        return isAllowed;
    }

/*
 * 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 com.xpn.xwiki.objects.classes;

import javax.inject.Provider;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.xwiki.model.document.DocumentAuthors;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.rendering.syntax.Syntax;
import org.xwiki.security.authorization.Right;
import org.xwiki.test.annotation.ComponentList;
import org.xwiki.test.junit5.LogCaptureExtension;
import org.xwiki.test.junit5.mockito.MockComponent;
import org.xwiki.user.GuestUserReference;
import org.xwiki.user.SuperAdminUserReference;

import com.xpn.xwiki.XWikiContext;
import com.xpn.xwiki.doc.XWikiDocument;
import com.xpn.xwiki.internal.render.OldRendering;
import com.xpn.xwiki.internal.security.authorization.DefaultAuthorExecutor;
import com.xpn.xwiki.internal.velocity.VelocityEvaluator;
import com.xpn.xwiki.objects.BaseObject;
import com.xpn.xwiki.test.MockitoOldcore;
import com.xpn.xwiki.test.junit5.mockito.InjectMockitoOldcore;
import com.xpn.xwiki.test.junit5.mockito.OldcoreTest;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * Unit tests for the {@link TextAreaClass} class.
 *
 * @version $Id$
 */
@OldcoreTest
@ComponentList({ DefaultAuthorExecutor.class })
class TextAreaClassTest
{
    private static final String PROPERTY_NAME = "test";

    @InjectMockitoOldcore
    private MockitoOldcore oldcore;

    @MockComponent
    private Provider<OldRendering> oldRenderingProvider;

    @MockComponent
    private VelocityEvaluator velocityEvaluator;

    @RegisterExtension
    private final LogCaptureExtension logCaptureExtension = new LogCaptureExtension();

    @Test
    void viewWikiText()
    {
        // Use a spy, so we don't need to mess around with clone-support in a mock.
        XWikiDocument spyDocument = getSpyDocument();

        TextAreaClass textAreaClass = new TextAreaClass();
        textAreaClass.setContentType(TextAreaClass.ContentType.WIKI_TEXT);
        BaseObject object = new BaseObject();
        object.setOwnerDocument(spyDocument);
        object.setLargeStringValue(PROPERTY_NAME, "**Test bold**");
        StringBuffer buffer = new StringBuffer();
        String renderingResult = "<p><strong>Test bold</strong></p>";
        doAnswer(invocationOnMock -> {
            XWikiDocument sDoc = invocationOnMock.getArgument(3);
            // Verify that the content author is set to the metadata author.
            assertEquals(GuestUserReference.INSTANCE, sDoc.getAuthors().getContentAuthor());
            return renderingResult;
        }).when(spyDocument).getRenderedContent(anyString(), any(Syntax.class), anyBoolean(), any(XWikiDocument.class),
            anyBoolean(), any(XWikiContext.class));
        textAreaClass.displayView(buffer, PROPERTY_NAME, "", object, true, this.oldcore.getXWikiContext());

        verify(spyDocument).getRenderedContent(anyString(), any(Syntax.class), anyBoolean(),
            any(XWikiDocument.class), anyBoolean(), any(XWikiContext.class));

        assertEquals(renderingResult, buffer.toString());
    }

    @ParameterizedTest
    @ValueSource(booleans = { true, false })
    void viewVelocityCode(boolean allowExecution)
    {
        XWikiDocument spyDocument = getSpyDocument();
        // Velocity code is only supported in XWiki 1.0.
        spyDocument.setSyntax(Syntax.XWIKI_1_0);

        TextAreaClass textAreaClass = new TextAreaClass();
        textAreaClass.setContentType(TextAreaClass.ContentType.VELOCITY_CODE);

        BaseObject object = new BaseObject();
        object.setOwnerDocument(spyDocument);
        String velocityCode = "#set($x = 1) $1 & 1";
        object.setLargeStringValue(PROPERTY_NAME, velocityCode);
        StringBuffer buffer = new StringBuffer();

        when(this.oldcore.getMockContextualAuthorizationManager().hasAccess(Right.SCRIPT)).then(invocationOnMock -> {
            // Verify that the content author is set to the metadata author.
            XWikiDocument sDoc = (XWikiDocument) this.oldcore.getXWikiContext().get(XWikiDocument.CKEY_SDOC);
            assertEquals(GuestUserReference.INSTANCE, sDoc.getAuthors().getContentAuthor());
            return allowExecution;
        });

        String renderingResult = "1 &#38; 1";
        OldRendering oldRendering = mock();
        String renderingInput = velocityCode.replace("&", "&#38;");
        when(oldRendering.parseContent(renderingInput, this.oldcore.getXWikiContext()))
            .thenReturn(renderingResult);
        when(this.oldRenderingProvider.get()).thenReturn(oldRendering);

        textAreaClass.displayView(buffer, PROPERTY_NAME, "", object, true, this.oldcore.getXWikiContext());

        if (allowExecution) {
            assertEquals(renderingResult, buffer.toString());
        } else {
            assertEquals(renderingInput, buffer.toString());
            verify(oldRendering, never()).parseContent(anyString(), any());
        }

        verify(this.oldcore.getMockContextualAuthorizationManager()).hasAccess(Right.SCRIPT);
    }

    @ParameterizedTest
    @ValueSource(booleans = { true, false })
    void viewVelocityWiki(boolean allowExecution)
    {
        XWikiDocument spyDocument = getSpyDocument();

        TextAreaClass textAreaClass = new TextAreaClass();
        textAreaClass.setContentType(TextAreaClass.ContentType.VELOCITYWIKI);

        BaseObject object = new BaseObject();
        object.setOwnerDocument(spyDocument);
        String velocityCode = "#set($x = \"bold\") **$1**";
        object.setLargeStringValue(PROPERTY_NAME, velocityCode);
        StringBuffer buffer = new StringBuffer();

        when(this.oldcore.getMockContextualAuthorizationManager().hasAccess(Right.SCRIPT)).then(invocationOnMock -> {
            // Verify that the content author is set to the metadata author.
            XWikiDocument sDoc = (XWikiDocument) this.oldcore.getXWikiContext().get(XWikiDocument.CKEY_SDOC);
            assertEquals(GuestUserReference.INSTANCE, sDoc.getAuthors().getContentAuthor());
            return allowExecution;
        });

        String velocityResult = "**bold**";
        when(this.velocityEvaluator.evaluateVelocityNoException(velocityCode, spyDocument.getDocumentReference()))
            .thenReturn(velocityResult);

        String renderingResult = "<p><strong>bold</strong></p>";

        // Mock the rendering of the result of the Velocity code. Check that the content author is set to the
        // metadata author.
        doAnswer(invocationOnMock -> {
            // Verify that the input is as expected.
            if (allowExecution) {
                assertEquals(velocityResult, invocationOnMock.getArgument(0));
            } else {
                assertEquals(velocityCode, invocationOnMock.getArgument(0));
            }

            XWikiDocument sDoc = invocationOnMock.getArgument(3);
            assertEquals(GuestUserReference.INSTANCE, sDoc.getAuthors().getContentAuthor());

            return renderingResult;
        }).when(spyDocument).getRenderedContent(anyString(), same(Syntax.XWIKI_2_1), anyBoolean(),
            any(XWikiDocument.class), anyBoolean(), any(XWikiContext.class));

        textAreaClass.displayView(buffer, PROPERTY_NAME, "", object, true, this.oldcore.getXWikiContext());

        assertEquals(renderingResult, buffer.toString());

        // Verify that script right was actually checked.
        verify(this.oldcore.getMockContextualAuthorizationManager()).hasAccess(Right.SCRIPT);
    }

    @Test
    void viewVelocityWikiWithoutOwnerDocument()
    {
        TextAreaClass textAreaClass = new TextAreaClass();
        textAreaClass.setContentType(TextAreaClass.ContentType.VELOCITYWIKI);

        BaseObject object = new BaseObject();
        String velocityCode = "1 & 2";
        object.setLargeStringValue(PROPERTY_NAME, velocityCode);
        StringBuffer buffer = new StringBuffer();

        textAreaClass.displayView(buffer, PROPERTY_NAME, "", object, true, this.oldcore.getXWikiContext());

        assertEquals("1 &#38; 2", buffer.toString());

        verify(this.oldcore.getMockContextualAuthorizationManager(), never()).hasAccess(Right.SCRIPT);

        assertEquals(1, this.logCaptureExtension.size());
        assertEquals("Error while getting the syntax corresponding to object [null]."
                + " Defaulting to using XWiki 1.0 syntax. Internal error [NullPointerException: ]",
            this.logCaptureExtension.getMessage(0));
    }

    private XWikiDocument getSpyDocument()
    {
        // Use a spy, so we don't need to mess around with clone-support in a mock.
        XWikiDocument spyDocument = spy(new XWikiDocument(new DocumentReference("wiki", "space", "page")));
        spyDocument.setSyntax(Syntax.XWIKI_2_1);
        this.oldcore.getXWikiContext().setDoc(spyDocument);
        // Add some authors to the document to verify that TextAreaClass is correctly setting the content author to
        // the metadata author.
        DocumentAuthors authors = spyDocument.getAuthors();
        authors.setEffectiveMetadataAuthor(GuestUserReference.INSTANCE);
        authors.setContentAuthor(SuperAdminUserReference.INSTANCE);
        return spyDocument;
    }
}