CVE-2023-36470: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection') (CWE-74)
value: XWiki Platform is a generic wiki platform offering runtime services for applications built on top of it. By either creating a new or editing an existing document with an icon set, an attacker can inject XWiki syntax and Velocity code that is executed with programming rights and thus allows remote code execution. There are different attack vectors, the simplest is the Velocity code in the icon set's HTML or XWiki syntax definition. The [icon picker](https://extensions.xwiki.org/xwiki/bin/view/Extension/Icon%20Theme%20Application#HIconPicker) can be used to trigger the rendering of any icon set. The XWiki syntax variant of the icon set is also used without any escaping in some documents, allowing to inject XWiki syntax including script macros into a document that might have programming right, for this the currently used icon theme needs to be edited. Further, the HTML output of the icon set is output as JSON in the icon picker and this JSON is interpreted as XWiki syntax, allowing again the injection of script macros into a document with programming right and thus allowing remote code execution. This impacts the confidentiality, integrity and availability of the whole XWiki instance. This issue has been patched in XWiki 14.10.6 and 15.1. Icon themes now require script right and the code in the icon theme is executed within the context of the icon theme, preventing any rights escalation. A macro for displaying icons has been introduced to avoid injecting the raw wiki syntax of an icon set into another document. Users are advised to upgrade. There are no known workarounds for this vulnerability.

CWE description: The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.
CVSS v3 base score: 8.8
Published: 2023-06-29T21:15Z
Repository: https://github.com/xwiki/xwiki-platform
Language: Java
Files changed: xwiki-platform-core/xwiki-platform-bridge/pom.xml, xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/internal/DefaultDocumentContextExecutor.java, xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/internal/DocumentContextExecutor.java, xwiki-platform-core/xwiki-platform-bridge/src/main/resources/META-INF/components.txt, xwiki-platform-core/xwiki-platform-bridge/src/test/java/org/xwiki/bridge/internal/DefaultDocumentContextExecutorTest.java, xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-api/src/main/java/org/xwiki/icon/IconException.java, xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-api/src/main/java/org/xwiki/icon/IconSet.java, xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/pom.xml, xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/DefaultIconRenderer.java, xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/DefaultIconSetLoader.java, xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/DefaultIconSetManager.java, xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/VelocityRenderer.java, xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/DefaultIconRendererTest.java, xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/DefaultIconSetLoaderTest.java, xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/DefaultIconSetManagerTest.java, xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/VelocityRendererTest.java, xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-rest/xwiki-platform-icon-rest-default/src/test/java/org/xwiki/icon/internal/DefaultIconThemesResourceTest.java, xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/main/java/org/xwiki/rendering/internal/util/XWikiSyntaxEscaper.java, xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/main/java/org/xwiki/rendering/script/RenderingScriptService.java, xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/main/resources/META-INF/components.txt, xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/test/java/org/xwiki/rendering/script/RenderingScriptServiceTest.java
Commit date: 2023-03-02 18:53:18 +0100
Commit message: XWIKI-20524: Improve icon rendering

* Require script right for icon set authors.
* Record the icon set's source document reference in the icon theme.
* Refactor XWiki syntax escaping into its own component. Currently, this
  component is internal as this is not the right place for it.
* Execute the Velocity code in the context of the icon theme's source
  document.
* Introduce a version of IconException that just takes a string.
* Add a new DocumentContextExecutor component for executing a Callable
  with a document in context.
* Add tests.

Diff: diff --git a/xwiki-platform-core/xwiki-platform-bridge/pom.xml b/xwiki-platform-core/xwiki-platform-bridge/pom.xml
index 709b4341aa488..a688d576a7afa 100644
--- a/xwiki-platform-core/xwiki-platform-bridge/pom.xml
+++ b/xwiki-platform-core/xwiki-platform-bridge/pom.xml
@@ -30,11 +30,11 @@
   <artifactId>xwiki-platform-bridge</artifactId>
   <name>XWiki Platform - Bridge</name>
   <packaging>jar</packaging>
   <description>Temporary bridge between new components and the old core, until the old core is completely split into components</description>
   <properties>
-    <xwiki.jacoco.instructionRatio>0.39</xwiki.jacoco.instructionRatio>
+    <xwiki.jacoco.instructionRatio>0.50</xwiki.jacoco.instructionRatio>
     <!-- Name to display by the Extension Manager -->
     <xwiki.extension.name>Model Bridge API</xwiki.extension.name>
   </properties>
   <dependencies>
     <dependency>
diff --git a/xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/internal/DefaultDocumentContextExecutor.java b/xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/internal/DefaultDocumentContextExecutor.java
new file mode 100644
index 0000000000000..06fe6980c5601
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/internal/DefaultDocumentContextExecutor.java
@@ -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.bridge.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.xwiki.bridge.DocumentAccessBridge;
+import org.xwiki.bridge.DocumentModelBridge;
+import org.xwiki.component.annotation.Component;
+import org.xwiki.model.ModelContext;
+import org.xwiki.model.reference.EntityReference;
+
+/**
+ * Default implementation of {@link DocumentContextExecutor}.
+ *
+ * @version $Id$
+ * @since 14.10.6
+ * @since 15.2RC1
+ */
+@Component
+@Singleton
+public class DefaultDocumentContextExecutor implements DocumentContextExecutor
+{
+    @Inject
+    private DocumentAccessBridge documentAccessBridge;
+
+    @Inject
+    private ModelContext modelContext;
+
+    @Override
+    public <V> V call(Callable<V> callable, DocumentModelBridge document) throws Exception
+    {
+        Map<String, Object> backupObjects = new HashMap<>();
+        EntityReference currentWikiReference = this.modelContext.getCurrentEntityReference();
+        boolean canPop = false;
+
+        try {
+            this.documentAccessBridge.pushDocumentInContext(backupObjects, document);
+            canPop = true;
+            // Make sure to synchronize the context wiki with the context document's wiki.
+            this.modelContext.setCurrentEntityReference(document.getDocumentReference().getWikiReference());
+
+            return callable.call();
+        } finally {
+            if (canPop) {
+                this.documentAccessBridge.popDocumentFromContext(backupObjects);
+                // Also restore the context wiki.
+                this.modelContext.setCurrentEntityReference(currentWikiReference);
+            }
+        }
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/internal/DocumentContextExecutor.java b/xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/internal/DocumentContextExecutor.java
new file mode 100644
index 0000000000000..1aa7759492da0
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/internal/DocumentContextExecutor.java
@@ -0,0 +1,46 @@
+/*
+ * 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.bridge.internal;
+
+import java.util.concurrent.Callable;
+
+import org.xwiki.bridge.DocumentModelBridge;
+import org.xwiki.component.annotation.Role;
+
+/**
+ * Executes a {@link Callable} with the given document in context.
+ *
+ * @version $Id$
+ * @since 14.10.6
+ * @since 15.2RC1
+ */
+@Role
+public interface DocumentContextExecutor
+{
+    /**
+     * Execute the passed {@link Callable} with the given document in context.
+     *
+     * @param callable the task to execute
+     * @param document the document to put in context
+     * @return computed result
+     * @param <V> the result type of method {@code call}
+     */
+    <V> V call(Callable<V> callable, DocumentModelBridge document) throws Exception;
+}
diff --git a/xwiki-platform-core/xwiki-platform-bridge/src/main/resources/META-INF/components.txt b/xwiki-platform-core/xwiki-platform-bridge/src/main/resources/META-INF/components.txt
new file mode 100644
index 0000000000000..9516bfc1832b6
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-bridge/src/main/resources/META-INF/components.txt
@@ -0,0 +1 @@
+org.xwiki.bridge.internal.DefaultDocumentContextExecutor
diff --git a/xwiki-platform-core/xwiki-platform-bridge/src/test/java/org/xwiki/bridge/internal/DefaultDocumentContextExecutorTest.java b/xwiki-platform-core/xwiki-platform-bridge/src/test/java/org/xwiki/bridge/internal/DefaultDocumentContextExecutorTest.java
new file mode 100644
index 0000000000000..946e724304e72
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-bridge/src/test/java/org/xwiki/bridge/internal/DefaultDocumentContextExecutorTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.bridge.internal;
+
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InOrder;
+import org.mockito.Mock;
+import org.xwiki.bridge.DocumentAccessBridge;
+import org.xwiki.bridge.DocumentModelBridge;
+import org.xwiki.model.EntityType;
+import org.xwiki.model.ModelContext;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.model.reference.EntityReference;
+import org.xwiki.model.reference.WikiReference;
+import org.xwiki.test.junit5.mockito.ComponentTest;
+import org.xwiki.test.junit5.mockito.InjectMockComponents;
+import org.xwiki.test.junit5.mockito.MockComponent;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit test for {@link DefaultDocumentContextExecutor}.
+ *
+ * @version $Id$
+ */
+@ComponentTest
+class DefaultDocumentContextExecutorTest
+{
+    private static final WikiReference TEST_WIKI_REFERENCE = new WikiReference("xwiki");
+
+    private static final DocumentReference TEST_DOCUMENT_REFERENCE =
+        new DocumentReference(TEST_WIKI_REFERENCE.getName(), "Space", "WebHome");
+
+    private static final WikiReference INITIAL_WIKI_REFERENCE = new WikiReference("initial");
+
+    @MockComponent
+    private ModelContext modelContext;
+
+    @MockComponent
+    private DocumentAccessBridge documentAccessBridge;
+
+    @InjectMockComponents
+    private DefaultDocumentContextExecutor executor;
+
+    @Mock
+    private Callable<Object> mockCallable;
+
+    @Mock
+    private DocumentModelBridge documentModelBridge;
+
+    private WikiReference modelWikiReference;
+
+    @BeforeEach
+    public void beforeEach()
+    {
+        when(this.documentModelBridge.getDocumentReference()).thenReturn(TEST_DOCUMENT_REFERENCE);
+
+        // Mock actually storing the wiki reference
+        this.modelWikiReference = INITIAL_WIKI_REFERENCE;
+        when(this.modelContext.getCurrentEntityReference()).thenReturn(this.modelWikiReference);
+        doAnswer(invocationOnMock -> {
+            EntityReference entityWikiReference =
+                invocationOnMock.getArgument(0, EntityReference.class).extractReference(EntityType.WIKI);
+            this.modelWikiReference = new WikiReference(entityWikiReference.getName());
+            return null;
+        }).when(this.modelContext).setCurrentEntityReference(any(EntityReference.class));
+    }
+
+    @Test
+    void call() throws Exception
+    {
+        String callResult = "callResult";
+        when(this.mockCallable.call()).thenReturn(callResult);
+        AtomicReference<Map<String, Object>> backupObjects = new AtomicReference<>();
+        doAnswer(invocationOnMock -> {
+            backupObjects.set(invocationOnMock.getArgument(0));
+            return null;
+        }).when(this.documentAccessBridge).pushDocumentInContext(any(), same(this.documentModelBridge));
+
+        Object actualResult = this.executor.call(this.mockCallable, this.documentModelBridge);
+
+        assertSame(callResult, actualResult);
+        InOrder inOrder = inOrder(this.documentAccessBridge, this.modelContext);
+        inOrder.verify(this.documentAccessBridge).pushDocumentInContext(any(), same(this.documentModelBridge));
+        inOrder.verify(this.modelContext).setCurrentEntityReference(TEST_WIKI_REFERENCE);
+        inOrder.verify(this.documentAccessBridge).popDocumentFromContext(backupObjects.get());
+        inOrder.verify(this.modelContext).setCurrentEntityReference(INITIAL_WIKI_REFERENCE);
+    }
+
+    @Test
+    void popWhenCallableThrows() throws Exception
+    {
+        Exception testException = new Exception("Callable failed");
+        when(this.mockCallable.call()).thenThrow(testException);
+
+        assertThrows(Exception.class, () -> this.executor.call(this.mockCallable, this.documentModelBridge));
+        InOrder inOrder = inOrder(this.documentAccessBridge, this.modelContext);
+        inOrder.verify(this.modelContext).setCurrentEntityReference(TEST_WIKI_REFERENCE);
+        inOrder.verify(this.documentAccessBridge).popDocumentFromContext(any());
+        inOrder.verify(this.modelContext).setCurrentEntityReference(INITIAL_WIKI_REFERENCE);
+    }
+
+    @Test
+    void doNothingWhenPushThrows() throws Exception
+    {
+        Exception testException = new Exception("Test");
+        doThrow(testException)
+            .when(this.documentAccessBridge).pushDocumentInContext(any(), same(this.documentModelBridge));
+
+        assertThrows(Exception.class, () -> this.executor.call(this.mockCallable, this.documentModelBridge));
+
+        verifyNoInteractions(this.mockCallable);
+        verify(this.modelContext, never()).setCurrentEntityReference(any());
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-api/src/main/java/org/xwiki/icon/IconException.java b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-api/src/main/java/org/xwiki/icon/IconException.java
index 0a62dd6b5d8b9..48a43c7e3d863 100644
--- a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-api/src/main/java/org/xwiki/icon/IconException.java
+++ b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-api/src/main/java/org/xwiki/icon/IconException.java
@@ -35,6 +35,18 @@ public class IconException extends Exception
      */
     public IconException(String message, Throwable source)
     {
         super(message, source);
     }
+
+    /**
+     * Constructor with just a message.
+     *
+     * @param message the message to store in the exception
+     * @since 14.10.6
+     * @since 15.2RC1
+     */
+    public IconException(String message)
+    {
+        super(message);
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-api/src/main/java/org/xwiki/icon/IconSet.java b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-api/src/main/java/org/xwiki/icon/IconSet.java
index ad160873f7608..13cedb085d01e 100644
--- a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-api/src/main/java/org/xwiki/icon/IconSet.java
+++ b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-api/src/main/java/org/xwiki/icon/IconSet.java
@@ -22,10 +22,13 @@
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.stability.Unstable;
+
 /**
  * A collection of icons, with some properties to display them.
  *
  * @version $Id$
  * @since 6.2M1
@@ -50,10 +53,12 @@
 
     private String iconCssClass;
 
     private IconType type;
 
+    private DocumentReference sourceDocumentReference;
+
     /**
      * Constructor.
      *
      * @param name name of the icon set
      */
@@ -266,6 +271,29 @@ public List<String> getIconNames()
      */
     public boolean hasIcon(String iconName)
     {
         return this.iconMap.containsKey(iconName);
     }
+
+    /**
+     * @return the document reference of the source of the icon theme, may be {@code null} if the icon set hasn't
+     * been loaded from a document
+     * @since 14.10.6
+     * @since 15.2RC1
+     */
+    @Unstable
+    public DocumentReference getSourceDocumentReference()
+    {
+        return this.sourceDocumentReference;
+    }
+
+    /**
+     * @param sourceDocumentReference the reference to the source of the icon theme
+     * @since 14.10.6
+     * @since 15.2RC1
+     */
+    @Unstable
+    public void setSourceDocumentReference(DocumentReference sourceDocumentReference)
+    {
+        this.sourceDocumentReference = sourceDocumentReference;
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/pom.xml b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/pom.xml
index b4160b851328b..e1787f6ac1fad 100644
--- a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/pom.xml
+++ b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/pom.xml
@@ -57,10 +57,15 @@
     <dependency>
       <groupId>org.xwiki.platform</groupId>
       <artifactId>xwiki-platform-wiki-api</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-user-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
 
     <!-- Test Dependencies -->
     <dependency>
       <groupId>javax.servlet</groupId>
       <artifactId>javax.servlet-api</artifactId>
diff --git a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/DefaultIconRenderer.java b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/DefaultIconRenderer.java
index 1837ec60c5d06..142f9bbc94d88 100644
--- a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/DefaultIconRenderer.java
+++ b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/DefaultIconRenderer.java
@@ -96,18 +96,18 @@ public String render(String iconName, IconSet iconSet, String renderer) throws I
         contentToParse.write("#set($icon = \"");
         contentToParse.write(icon.getValue());
         contentToParse.write("\")\n");
         contentToParse.write(renderer);
 
-        return velocityRenderer.render(contentToParse.toString());
+        return this.velocityRenderer.render(contentToParse.toString(), iconSet.getSourceDocumentReference());
     }
 
     @Override
     public void use(IconSet iconSet) throws IconException
     {
         if (iconSet == null) {
-            throw new IconException("The icon set is null", null);
+            throw new IconException("The icon set is null");
         }
         if (!StringUtils.isBlank(iconSet.getCss())) {
             activeCSS(iconSet);
         }
         if (!StringUtils.isBlank(iconSet.getSsx())) {
@@ -118,21 +118,21 @@ public void use(IconSet iconSet) throws IconException
         }
     }
 
     private void activeCSS(IconSet iconSet) throws IconException
     {
-        String url = velocityRenderer.render(iconSet.getCss());
-        Map<String, Object> parameters = new HashMap();
+        String url = this.velocityRenderer.render(iconSet.getCss(), iconSet.getSourceDocumentReference());
+        Map<String, Object> parameters = new HashMap<>();
         parameters.put("rel", "stylesheet");
-        linkExtension.use(url, parameters);
+        this.linkExtension.use(url, parameters);
     }
 
     private void activeSSX(IconSet iconSet)
     {
-        skinExtension.use(iconSet.getSsx());
+        this.skinExtension.use(iconSet.getSsx());
     }
 
     private void activeJSX(IconSet iconSet)
     {
-        jsExtension.use(iconSet.getJsx());
+        this.jsExtension.use(iconSet.getJsx());
     }
 }
diff --git a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/DefaultIconSetLoader.java b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/DefaultIconSetLoader.java
index cee123744a6cd..6bf051d9d0b8c 100644
--- a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/DefaultIconSetLoader.java
+++ b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/DefaultIconSetLoader.java
@@ -23,10 +23,11 @@
 import java.io.Reader;
 import java.io.StringReader;
 import java.util.Properties;
 
 import javax.inject.Inject;
+import javax.inject.Named;
 import javax.inject.Singleton;
 
 import org.xwiki.bridge.DocumentAccessBridge;
 import org.xwiki.bridge.DocumentModelBridge;
 import org.xwiki.component.annotation.Component;
@@ -34,10 +35,13 @@
 import org.xwiki.icon.IconException;
 import org.xwiki.icon.IconSet;
 import org.xwiki.icon.IconSetLoader;
 import org.xwiki.icon.IconType;
 import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.security.authorization.AuthorizationManager;
+import org.xwiki.security.authorization.Right;
+import org.xwiki.user.UserReferenceSerializer;
 import org.xwiki.wiki.descriptor.WikiDescriptorManager;
 
 /**
  * Default implementation of {@link org.xwiki.icon.IconSetLoader}.
  *
@@ -70,23 +74,42 @@ public class DefaultIconSetLoader implements IconSetLoader
     private DocumentAccessBridge documentAccessBridge;
 
     @Inject
     private WikiDescriptorManager wikiDescriptorManager;
 
+    @Inject
+    private AuthorizationManager authorizationManager;
+
+    @Inject
+    @Named("document")
+    private UserReferenceSerializer<DocumentReference> documentUserSerializer;
+
     @Override
     public IconSet loadIconSet(DocumentReference iconSetReference) throws IconException
     {
         try {
             // Get the document
-            DocumentModelBridge doc = documentAccessBridge.getDocumentInstance(iconSetReference);
+            DocumentModelBridge doc = this.documentAccessBridge.getDocumentInstance(iconSetReference);
+
+            // Check that both the content (actual icon theme content) and the metadata author (icon theme object)
+            // have script right.
+            DocumentReference contentAuthor =
+                this.documentUserSerializer.serialize(doc.getAuthors().getContentAuthor());
+            this.authorizationManager.checkAccess(Right.SCRIPT, contentAuthor, iconSetReference);
+            DocumentReference metadataAuthor =
+                this.documentUserSerializer.serialize(doc.getAuthors().getEffectiveMetadataAuthor());
+            this.authorizationManager.checkAccess(Right.SCRIPT, metadataAuthor, iconSetReference);
+
             String content = doc.getContent();
             // The name of the icon set is stored in the IconThemesCode.IconThemeClass XObject of the document
-            DocumentReference iconClassRef = new DocumentReference(wikiDescriptorManager.getCurrentWikiId(),
+            DocumentReference iconClassRef = new DocumentReference(this.wikiDescriptorManager.getCurrentWikiId(),
                 "IconThemesCode", "IconThemeClass");
-            String name = (String) documentAccessBridge.getProperty(iconSetReference, iconClassRef, "name");
+            String name = (String) this.documentAccessBridge.getProperty(iconSetReference, iconClassRef, "name");
             // Load the icon set
-            return loadIconSet(new StringReader(content), name);
+            IconSet result = loadIconSet(new StringReader(content), name);
+            result.setSourceDocumentReference(iconSetReference);
+            return result;
         } catch (Exception e) {
             throw new IconException(String.format(ERROR_MSG, iconSetReference), e);
         }
     }
 
diff --git a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/DefaultIconSetManager.java b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/DefaultIconSetManager.java
index 4e704aa4664bf..57a2834a4728c 100644
--- a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/DefaultIconSetManager.java
+++ b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/DefaultIconSetManager.java
@@ -19,18 +19,20 @@
  */
 package org.xwiki.icon.internal;
 
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.util.ArrayList;
 import java.util.List;
 
 import javax.inject.Inject;
 import javax.inject.Named;
 import javax.inject.Provider;
 import javax.inject.Singleton;
 
 import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
 import org.xwiki.bridge.DocumentAccessBridge;
 import org.xwiki.component.annotation.Component;
 import org.xwiki.configuration.ConfigurationSource;
 import org.xwiki.icon.IconException;
 import org.xwiki.icon.IconSet;
@@ -46,10 +48,12 @@
 import org.xwiki.wiki.descriptor.WikiDescriptorManager;
 
 import com.xpn.xwiki.XWiki;
 import com.xpn.xwiki.XWikiContext;
 
+import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage;
+
 /**
  * Default implementation of {@link org.xwiki.icon.IconSetManager}.
  *
  * @since 6.2M1
  * @version $Id$
@@ -87,10 +91,13 @@ public class DefaultIconSetManager implements IconSetManager
     
     @Inject
     @Named("all")
     private ConfigurationSource configurationSource;
 
+    @Inject
+    private Logger logger;
+
     @Override
     public IconSet getCurrentIconSet() throws IconException
     {
         // Check the context
         IconSet iconSet = this.iconSetContext.getIconSet();
@@ -155,40 +162,73 @@ public IconSet getIconSet(String name) throws IconException
         if (DEFAULT_ICONSET_NAME.equals(name)) {
             return getDefaultIconSet();
         }
 
         // Get the icon set from the cache
-        IconSet iconSet = iconSetCache.get(name, wikiDescriptorManager.getCurrentWikiId());
+        IconSet iconSet = this.iconSetCache.get(name, this.wikiDescriptorManager.getCurrentWikiId());
 
         // Load it if it is not loaded yet
         if (iconSet == null) {
+            List<String> results;
+
             try {
                 // Search by name
                 String xwql = "FROM doc.object(IconThemesCode.IconThemeClass) obj WHERE obj.name = :name";
-                Query query = queryManager.createQuery(xwql, Query.XWQL);
+                Query query = this.queryManager.createQuery(xwql, Query.XWQL);
                 query.bindValue("name", name);
-                List<String> results = query.execute();
-                if (results.isEmpty()) {
-                    return null;
-                }
+                results = query.execute();
+            } catch (QueryException e) {
+                throw new IconException(String.format("Failed to load the icon set [%s].", name), e);
+            }
+
+            iconSet = loadIconSetFromCandidateDocuments(name, results);
+        }
+
+        // Return the icon set
+        return iconSet;
+    }
+
+    private IconSet loadIconSetFromCandidateDocuments(String name, List<String> candidateDocuments) throws IconException
+    {
+        List<IconException> iconExceptions = new ArrayList<>();
+        IconSet iconSet = null;
 
-                // Get the first result
-                String docName = results.get(0);
-                DocumentReference docRef = documentReferenceResolver.resolve(docName);
+        // Try all results to find the first one that loads successfully.
+        for (String docName : candidateDocuments) {
+            DocumentReference docRef = this.documentReferenceResolver.resolve(docName);
 
+            try {
                 // Load the icon theme
-                iconSet = iconSetLoader.loadIconSet(docRef);
+                iconSet = this.iconSetLoader.loadIconSet(docRef);
 
                 // Put it in the cache
-                iconSetCache.put(docRef, iconSet);
-                iconSetCache.put(name, wikiDescriptorManager.getCurrentWikiId(), iconSet);
-            } catch (QueryException e) {
-                throw new IconException(String.format("Failed to load the icon set [%s].", name), e);
+                this.iconSetCache.put(docRef, iconSet);
+                this.iconSetCache.put(name, this.wikiDescriptorManager.getCurrentWikiId(), iconSet);
+
+                break;
+            } catch (IconException e) {
+                // Store the exception first, maybe there is another icon theme with the same name that loads
+                // successfully.
+                iconExceptions.add(e);
+            }
+        }
+
+        if (iconSet == null && !iconExceptions.isEmpty()) {
+            if (iconExceptions.size() > 1) {
+                iconExceptions.stream().skip(1)
+                    .forEach(e -> this.logger.warn("Failed loading icon set [{}] from multiple matching "
+                            + "documents, ignored this additional exception, reason: [{}].", name,
+                        getRootCauseMessage(e)));
+                throw new IconException(String.format("Failed to load the icon set [%s] from %d documents, "
+                        + "reporting the first exception, see the log for additional errors.",
+                    name, candidateDocuments.size()),
+                    iconExceptions.get(0));
+            } else {
+                throw iconExceptions.get(0);
             }
         }
 
-        // Return the icon set
         return iconSet;
     }
 
     @Override
     public List<String> getIconSetNames() throws IconException
diff --git a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/VelocityRenderer.java b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/VelocityRenderer.java
index 86c58c8810023..01a9e33dae659 100644
--- a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/VelocityRenderer.java
+++ b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/VelocityRenderer.java
@@ -18,22 +18,29 @@
  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  */
 package org.xwiki.icon.internal;
 
 import java.io.StringWriter;
+import java.util.concurrent.Callable;
 
 import javax.inject.Inject;
+import javax.inject.Named;
 import javax.inject.Singleton;
 
 import org.apache.velocity.VelocityContext;
+import org.xwiki.bridge.DocumentAccessBridge;
+import org.xwiki.bridge.DocumentModelBridge;
+import org.xwiki.bridge.internal.DocumentContextExecutor;
 import org.xwiki.component.annotation.Component;
 import org.xwiki.icon.IconException;
 import org.xwiki.logging.LoggerConfiguration;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.security.authorization.AuthorExecutor;
+import org.xwiki.user.UserReferenceSerializer;
 import org.xwiki.velocity.VelocityEngine;
 import org.xwiki.velocity.VelocityManager;
 import org.xwiki.velocity.XWikiVelocityContext;
-import org.xwiki.velocity.XWikiVelocityException;
 
 /**
  * Internal helper to render safely any velocity code.
  *
  * @version $Id$
@@ -41,58 +48,103 @@
  */
 @Component(roles = VelocityRenderer.class)
 @Singleton
 public class VelocityRenderer
 {
+    private static final String NAMESPACE = "DefaultIconRenderer";
+
     @Inject
     private VelocityManager velocityManager;
 
     @Inject
     private LoggerConfiguration loggerConfiguration;
 
+    @Inject
+    private DocumentAccessBridge documentAccessBridge;
+
+    @Inject
+    private AuthorExecutor authorExecutor;
+
+    @Inject
+    private DocumentContextExecutor documentContextExecutor;
+
+    @Inject
+    @Named("document")
+    private UserReferenceSerializer<DocumentReference> documentUserSerializer;
+
     /**
      * Render a velocity code without messing with the document context and namespace.
      * @param code code to render
+     * @param contextDocumentReference the reference of the context document
      * @return the rendered code
      * @throws IconException if problem occurs
      */
-    public String render(String code) throws IconException
+    public String render(String code, DocumentReference contextDocumentReference) throws IconException
     {
         // The macro namespace to use by the velocity engine, see afterwards.
         String namespace = "IconVelocityRenderer_" + Thread.currentThread().getId();
 
         // Create the output writer
         StringWriter output = new StringWriter();
 
         VelocityEngine engine = null;
+
+        boolean result;
+
         try {
             // Get the velocity engine
-            engine = velocityManager.getVelocityEngine();
+            engine = this.velocityManager.getVelocityEngine();
 
             // Use a new macro namespace to prevent the code redefining existing macro.
             // We use the thread name to have a unique id.
             engine.startedUsingMacroNamespace(namespace);
 
+            DocumentReference authorReference;
+            DocumentModelBridge sourceDocument;
+
+            // Execute the Velocity code in an isolated execution context with the rights of its author when the icon
+            // theme is from a document.
+            if (contextDocumentReference != null) {
+                sourceDocument =
+                    this.documentAccessBridge.getDocumentInstance(contextDocumentReference);
+                authorReference = this.documentUserSerializer.serialize(sourceDocument.getAuthors().getContentAuthor());
+            } else {
+                authorReference = null;
+                sourceDocument = null;
+            }
+
             // Create a new VelocityContext to prevent the code creating variables in the current context.
             // See https://jira.xwiki.org/browse/XWIKI-11400.
             // We set the current context as inner context of the new one to be able to read existing variables.
             // See https://jira.xwiki.org/browse/XWIKI-11426.
-            VelocityContext context = new XWikiVelocityContext(velocityManager.getVelocityContext(),
+            VelocityContext context = new XWikiVelocityContext(this.velocityManager.getVelocityContext(),
                 this.loggerConfiguration.isDeprecatedLogEnabled());
 
             // Render the code
-            if (engine.evaluate(context, output, "DefaultIconRenderer", code)) {
-                return output.toString();
-            } else {
-                // I don't know how to check the velocity runtime log
-                throw new IconException("Failed to render the icon. See the Velocity runtime log.", null);
+            VelocityEngine finalEngine = engine;
+            Callable<Boolean> callable = () -> finalEngine.evaluate(context, output, NAMESPACE, code);
+            if (contextDocumentReference != null) {
+                // Wrap the callable in a document context and author executor to ensure that the document is in
+                // context and the Velocity code is executed with the author's rights.
+                Callable<Boolean> innerCallable = callable;
+                callable = () -> this.documentContextExecutor.call(
+                    () -> this.authorExecutor.call(innerCallable, authorReference, contextDocumentReference),
+                    sourceDocument);
             }
-        } catch (XWikiVelocityException e) {
+            result = callable.call();
+        } catch (Exception e) {
             throw new IconException("Failed to render the icon.", e);
         } finally {
             // Do not forget to close the macro namespace we have created previously
             if (engine != null) {
                 engine.stoppedUsingMacroNamespace(namespace);
             }
         }
+
+        if (result) {
+            return output.toString();
+        } else {
+            // I don't know how to check the velocity runtime log
+            throw new IconException("Failed to render the icon. See the Velocity runtime log.");
+        }
     }
 }
diff --git a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/DefaultIconRendererTest.java b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/DefaultIconRendererTest.java
index fccb973ddaed5..213ed49aa8907 100644
--- a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/DefaultIconRendererTest.java
+++ b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/DefaultIconRendererTest.java
@@ -73,11 +73,12 @@
     public void render() throws Exception
     {
         IconSet iconSet = new IconSet("default");
         iconSet.setRenderWiki("image:$icon.png");
         iconSet.addIcon("test", new Icon("blabla"));
-        when(velocityRenderer.render("#set($icon = \"blabla\")\nimage:$icon.png")).thenReturn("image:blabla.png");
+        when(this.velocityRenderer.render("#set($icon = \"blabla\")\nimage:$icon.png", null))
+            .thenReturn("image:blabla.png");
 
         // Test
         String result = iconRenderer.render("test", iconSet);
 
         // Verify
@@ -92,11 +93,11 @@ public void renderWithCSS() throws Exception
     {
         IconSet iconSet = new IconSet("default");
         iconSet.setRenderWiki("image:$icon.png");
         iconSet.setCss("css");
         iconSet.addIcon("test", new Icon("blabla"));
-        when(velocityRenderer.render("css")).thenReturn("velocityParsedCSS");
+        when(this.velocityRenderer.render("css", null)).thenReturn("velocityParsedCSS");
 
         // Test
         iconRenderer.render("test", iconSet);
 
         // Verify
@@ -146,11 +147,11 @@ public void renderHTML() throws Exception
     {
         IconSet iconSet = new IconSet("default");
         iconSet.setRenderHTML("<img src=\"$icon.png\" />");
         iconSet.addIcon("test", new Icon("blabla"));
 
-        when(velocityRenderer.render("#set($icon = \"blabla\")\n<img src=\"$icon.png\" />"))
+        when(this.velocityRenderer.render("#set($icon = \"blabla\")\n<img src=\"$icon.png\" />", null))
             .thenReturn("<img src=\"blabla.png\" />");
 
         // Test
         String result = iconRenderer.renderHTML("test", iconSet);
 
@@ -163,11 +164,11 @@ public void renderHTMLWithCSS() throws Exception
     {
         IconSet iconSet = new IconSet("default");
         iconSet.setRenderHTML("<img src=\"$icon.png\" />");
         iconSet.setCss("css");
         iconSet.addIcon("test", new Icon("blabla"));
-        when(velocityRenderer.render("css")).thenReturn("velocityParsedCSS");
+        when(this.velocityRenderer.render("css", null)).thenReturn("velocityParsedCSS");
 
         // Test
         iconRenderer.renderHTML("test", iconSet);
 
         // Verify
@@ -228,12 +229,12 @@ public void renderNonExistentIcon() throws Exception
     public void renderWithException() throws Exception
     {
         IconSet iconSet = new IconSet("default");
         iconSet.setRenderWiki("image:$icon.png");
         iconSet.addIcon("test", new Icon("blabla"));
-        IconException exception = new IconException("exception", null);
-        when(velocityRenderer.render(any())).thenThrow(exception);
+        IconException exception = new IconException("exception");
+        when(this.velocityRenderer.render(any(), any())).thenThrow(exception);
 
         // Test
         IconException caughtException = null;
         try {
             iconRenderer.render("test", iconSet);
@@ -249,11 +250,11 @@ public void renderWithException() throws Exception
     @Test
     public void renderIcon() throws Exception
     {
         IconSet iconSet = new IconSet("iconSet");
         iconSet.addIcon("test", new Icon("hello"));
-        when(velocityRenderer.render("#set($icon = \"hello\")\nfa fa-$icon")).thenReturn("fa fa-hello");
+        when(this.velocityRenderer.render("#set($icon = \"hello\")\nfa fa-$icon", null)).thenReturn("fa fa-hello");
 
         // Test
         String renderedIcon1 = iconRenderer.render("test", iconSet, "fa fa-$icon");
         String renderedIcon2 = iconRenderer.render("none", iconSet, "fa fa-$icon");
         String renderedIcon3 = iconRenderer.render("none", null, "fa fa-$icon");
diff --git a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/DefaultIconSetLoaderTest.java b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/DefaultIconSetLoaderTest.java
index 61e91e2499a20..dd53570f84763 100644
--- a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/DefaultIconSetLoaderTest.java
+++ b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/DefaultIconSetLoaderTest.java
@@ -30,31 +30,34 @@
 import org.xwiki.bridge.DocumentAccessBridge;
 import org.xwiki.bridge.DocumentModelBridge;
 import org.xwiki.icon.IconException;
 import org.xwiki.icon.IconSet;
 import org.xwiki.icon.IconType;
+import org.xwiki.model.internal.document.DefaultDocumentAuthors;
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.test.junit5.mockito.ComponentTest;
 import org.xwiki.test.junit5.mockito.InjectMockComponents;
 import org.xwiki.test.junit5.mockito.MockComponent;
 import org.xwiki.wiki.descriptor.WikiDescriptorManager;
 
+import com.xpn.xwiki.doc.XWikiDocument;
+
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 /**
  * Test class for {@link org.xwiki.icon.internal.DefaultIconSetLoader}.
  *
  * @version $Id$
  * @since 6.2M1
  */
 @ComponentTest
-public class DefaultIconSetLoaderTest
+class DefaultIconSetLoaderTest
 {
     @InjectMockComponents
     private DefaultIconSetLoader iconSetLoader;
 
     @MockComponent
@@ -62,13 +65,13 @@
 
     @MockComponent
     private WikiDescriptorManager wikiDescriptorManager;
 
     @BeforeEach
-    public void setUp() throws Exception
+    void setUp()
     {
-        when(wikiDescriptorManager.getCurrentWikiId()).thenReturn("wikiId");
+        when(this.wikiDescriptorManager.getCurrentWikiId()).thenReturn("wikiId");
     }
 
     private void verifies(IconSet result) throws Exception
     {
         assertNotNull(result);
@@ -83,79 +86,68 @@ private void verifies(IconSet result) throws Exception
         assertEquals("globe", result.getIcon("earth").getValue());
         assertEquals(IconType.FONT, result.getType());
     }
 
     @Test
-    public void loadIconSet() throws Exception
+    void loadIconSet() throws Exception
     {
         Reader content = new InputStreamReader(getClass().getResourceAsStream("/test.iconset"));
 
         // Test
-        IconSet result = iconSetLoader.loadIconSet(content, "FontAwesome");
+        IconSet result = this.iconSetLoader.loadIconSet(content, "FontAwesome");
 
         // Verify
         verifies(result);
         assertEquals("FontAwesome", result.getName());
     }
 
     @Test
-    public void loadIconSetFromWikiDocument() throws Exception
+    void loadIconSetFromWikiDocument() throws Exception
     {
         DocumentReference iconSetRef = new DocumentReference("xwiki", "IconThemes", "Default");
         DocumentReference iconClassRef = new DocumentReference("wikiId", "IconThemesCode", "IconThemeClass");
-        when(documentAccessBridge.getProperty(eq(iconSetRef), eq(iconClassRef), eq("name"))).thenReturn("MyIconTheme");
+        when(this.documentAccessBridge.getProperty(iconSetRef, iconClassRef, "name")).thenReturn("MyIconTheme");
         DocumentModelBridge doc = mock(DocumentModelBridge.class);
-        when(documentAccessBridge.getDocumentInstance(iconSetRef)).thenReturn(doc);
+        when(this.documentAccessBridge.getDocumentInstance(iconSetRef)).thenReturn(doc);
 
         StringWriter content = new StringWriter();
         IOUtils.copyLarge(new InputStreamReader(getClass().getResourceAsStream("/test.iconset")), content);
         when(doc.getContent()).thenReturn(content.toString());
 
+        when(doc.getAuthors()).thenReturn(new DefaultDocumentAuthors(new XWikiDocument(iconSetRef)));
+
         // Test
-        IconSet result = iconSetLoader.loadIconSet(iconSetRef);
+        IconSet result = this.iconSetLoader.loadIconSet(iconSetRef);
 
         // Verify
         verifies(result);
         assertEquals("MyIconTheme", result.getName());
     }
 
     @Test
-    public void loadIconSetWithException() throws Exception
+    void loadIconSetWithException() throws Exception
     {
         Reader content = mock(Reader.class);
         IOException exception = new IOException("test");
         when(content.read(any(char[].class))).thenThrow(exception);
 
         // Test
-        Exception caughException = null;
-        try {
-            iconSetLoader.loadIconSet(content, "FontAwesome");
-        } catch (IconException e) {
-            caughException = e;
-        }
-
-        assertNotNull(caughException);
-        assert (caughException instanceof IconException);
-        assertEquals(exception, caughException.getCause());
-        assertEquals("Failed to load the IconSet [FontAwesome].", caughException.getMessage());
+        Exception caughtException = assertThrows(IconException.class, () ->
+            this.iconSetLoader.loadIconSet(content, "FontAwesome"));
+
+        assertEquals(exception, caughtException.getCause());
+        assertEquals("Failed to load the IconSet [FontAwesome].", caughtException.getMessage());
     }
 
     @Test
-    public void loadIconSetFromWikiDocumentWithException() throws Exception
+    void loadIconSetFromWikiDocumentWithException() throws Exception
     {
         Exception exception = new Exception("test");
-        when(documentAccessBridge.getDocumentInstance(any(DocumentReference.class))).thenThrow(exception);
+        when(this.documentAccessBridge.getDocumentInstance(any(DocumentReference.class))).thenThrow(exception);
 
-        // Test
-        Exception caughException = null;
-        try {
-            iconSetLoader.loadIconSet(new DocumentReference("a", "b", "c"));
-        } catch (IconException e) {
-            caughException = e;
-        }
-
-        assertNotNull(caughException);
-        assert (caughException instanceof IconException);
-        assertEquals(exception, caughException.getCause());
-        assertEquals("Failed to load the IconSet [a:b.c].", caughException.getMessage());
+        IconException caughtException = assertThrows(IconException.class, () ->
+            this.iconSetLoader.loadIconSet(new DocumentReference("a", "b", "c")));
+
+        assertEquals(exception, caughtException.getCause());
+        assertEquals("Failed to load the IconSet [a:b.c].", caughtException.getMessage());
     }
 }
diff --git a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/DefaultIconSetManagerTest.java b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/DefaultIconSetManagerTest.java
index 573d4ad94de1a..199bdd9cdde8a 100644
--- a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/DefaultIconSetManagerTest.java
+++ b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/DefaultIconSetManagerTest.java
@@ -29,10 +29,11 @@
 import javax.inject.Named;
 import javax.inject.Provider;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.RegisterExtension;
 import org.mockito.Mock;
 import org.xwiki.bridge.DocumentAccessBridge;
 import org.xwiki.configuration.ConfigurationSource;
 import org.xwiki.icon.IconException;
 import org.xwiki.icon.IconSet;
@@ -42,10 +43,12 @@
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.model.reference.DocumentReferenceResolver;
 import org.xwiki.query.Query;
 import org.xwiki.query.QueryException;
 import org.xwiki.query.QueryManager;
+import org.xwiki.test.LogLevel;
+import org.xwiki.test.junit5.LogCaptureExtension;
 import org.xwiki.test.junit5.mockito.ComponentTest;
 import org.xwiki.test.junit5.mockito.InjectMockComponents;
 import org.xwiki.test.junit5.mockito.MockComponent;
 import org.xwiki.wiki.descriptor.WikiDescriptorManager;
 
@@ -53,10 +56,11 @@
 import com.xpn.xwiki.XWikiContext;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyString;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
@@ -110,10 +114,13 @@
     private XWikiContext xcontext;
 
     @Mock
     private XWiki xwiki;
 
+    @RegisterExtension
+    private LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);
+
     @BeforeEach
     void setUp()
     {
         when(this.xcontextProvider.get()).thenReturn(this.xcontext);
         when(this.xcontext.getWiki()).thenReturn(this.xwiki);
@@ -324,10 +331,74 @@ void getIconSetWhenException() throws Exception
         assertNotNull(caughtException);
         assertEquals(exception, caughtException.getCause());
         assertEquals("Failed to load the icon set [silk].", caughtException.getMessage());
     }
 
+    @Test
+    void getIconSetWhenOneFails() throws Exception
+    {
+        // Mocks
+        IconSet iconSet = new IconSet("silk");
+        Query query = mock(Query.class);
+        when(this.queryManager.createQuery("FROM doc.object(IconThemesCode.IconThemeClass) obj WHERE obj.name = :name",
+            Query.XWQL)).thenReturn(query);
+        List<String> results = List.of("FakeIcon.Silk", "IconThemes.Silk");
+        when(query.<String>execute()).thenReturn(results);
+        DocumentReference fakeDocumentReference = new DocumentReference("wiki", "FakeIcon", "Silk");
+        when(this.documentReferenceResolver.resolve("FakeIcon.Silk")).thenReturn(fakeDocumentReference);
+        when(this.iconSetLoader.loadIconSet(fakeDocumentReference)).thenThrow(new IconException("Test"));
+
+        DocumentReference documentReference = new DocumentReference("wiki", "IconThemes", "Silk");
+        when(this.documentReferenceResolver.resolve("IconThemes.Silk")).thenReturn(documentReference);
+        when(this.iconSetLoader.loadIconSet(documentReference)).thenReturn(iconSet);
+
+        // Test
+        assertEquals(iconSet, this.iconSetManager.getIconSet("silk"));
+
+        // Verify
+        verify(query).bindValue("name", "silk");
+        verify(this.iconSetCache).put(documentReference, iconSet);
+        verify(this.iconSetCache).put("silk", "currentWikiId", iconSet);
+        verify(this.iconSetLoader).loadIconSet(fakeDocumentReference);
+    }
+
+    @Test
+    void getIconSetWhenAllFail() throws Exception
+    {
+        // Mocks
+        Query query = mock(Query.class);
+        when(this.queryManager.createQuery("FROM doc.object(IconThemesCode.IconThemeClass) obj WHERE obj.name = :name",
+            Query.XWQL)).thenReturn(query);
+        List<String> results = List.of("FakeIcon.Silk", "IconThemes.Silk");
+        when(query.<String>execute()).thenReturn(results);
+        DocumentReference fakeDocumentReference = new DocumentReference("wiki", "FakeIcon", "Silk");
+        when(this.documentReferenceResolver.resolve("FakeIcon.Silk")).thenReturn(fakeDocumentReference);
+        IconException fakeException = new IconException("Fake");
+        when(this.iconSetLoader.loadIconSet(fakeDocumentReference)).thenThrow(fakeException);
+
+        DocumentReference documentReference = new DocumentReference("wiki", "IconThemes", "Silk");
+        when(this.documentReferenceResolver.resolve("IconThemes.Silk")).thenReturn(documentReference);
+        when(this.iconSetLoader.loadIconSet(documentReference)).thenThrow(new IconException("Real"));
+
+        // Test
+        IconException exception = assertThrows(IconException.class, () -> this.iconSetManager.getIconSet("silk"));
+        assertEquals("Failed to load the icon set [silk] from 2 documents, reporting the first exception, see the"
+            + " log for additional errors.", exception.getMessage());
+        assertEquals(fakeException, exception.getCause());
+
+        assertEquals(1, this.logCapture.size());
+        assertEquals("Failed loading icon set [silk] from multiple matching documents, "
+                + "ignored this additional exception, reason: [IconException: Real].",
+            this.logCapture.getMessage(0));
+
+        // Verify
+        verify(query).bindValue("name", "silk");
+        verify(this.iconSetCache, never()).put(anyString(), any());
+        verify(this.iconSetCache, never()).put(any(DocumentReference.class), any());
+        verify(this.iconSetLoader).loadIconSet(fakeDocumentReference);
+    }
+
     @Test
     void getDefaultIconSet() throws Exception
     {
         // Mock
         IconSet iconSet = new IconSet("default");
diff --git a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/VelocityRendererTest.java b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/VelocityRendererTest.java
index 2d2d81739cd81..16a80c986f162 100644
--- a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/VelocityRendererTest.java
+++ b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/VelocityRendererTest.java
@@ -18,25 +18,37 @@
  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
  */
 package org.xwiki.icon.internal;
 
 import java.io.Writer;
+import java.util.concurrent.Callable;
+
+import javax.inject.Named;
 
 import org.apache.velocity.VelocityContext;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
+import org.junit.jupiter.api.Test;
+import org.xwiki.bridge.DocumentAccessBridge;
+import org.xwiki.bridge.internal.DocumentContextExecutor;
 import org.xwiki.icon.IconException;
-import org.xwiki.test.mockito.MockitoComponentMockingRule;
+import org.xwiki.model.document.DocumentAuthors;
+import org.xwiki.model.internal.document.DefaultDocumentAuthors;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.security.authorization.AuthorExecutor;
+import org.xwiki.test.junit5.mockito.ComponentTest;
+import org.xwiki.test.junit5.mockito.InjectMockComponents;
+import org.xwiki.test.junit5.mockito.MockComponent;
+import org.xwiki.user.UserReference;
+import org.xwiki.user.UserReferenceSerializer;
 import org.xwiki.velocity.VelocityEngine;
 import org.xwiki.velocity.VelocityManager;
 import org.xwiki.velocity.XWikiVelocityException;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
+import com.xpn.xwiki.doc.XWikiDocument;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -45,93 +57,122 @@
  * Test class for {@link org.xwiki.icon.internal.VelocityRenderer}.
  *
  * @since 6.4M1
  * @version $Id$
  */
-public class VelocityRendererTest
+@ComponentTest
+class VelocityRendererTest
 {
-    @Rule
-    public MockitoComponentMockingRule<VelocityRenderer> mocker =
-            new MockitoComponentMockingRule<>(VelocityRenderer.class);
-
+    @MockComponent
     private VelocityManager velocityManager;
 
-    @Before
-    public void setUp() throws Exception
-    {
-        velocityManager = mocker.getInstance(VelocityManager.class);
-    }
+    @MockComponent
+    private DocumentAccessBridge documentAccessBridge;
+
+    @MockComponent
+    @Named("document")
+    private UserReferenceSerializer<DocumentReference> documentUserSerializer;
+
+    @MockComponent
+    private AuthorExecutor authorExecutor;
+
+    @MockComponent
+    private DocumentContextExecutor documentContextExecutor;
+
+    @InjectMockComponents
+    private VelocityRenderer velocityRenderer;
 
     @Test
-    public void renderTest() throws Exception
+    void renderTest() throws Exception
     {
         // Mocks
         VelocityEngine engine = mock(VelocityEngine.class);
-        when(velocityManager.getVelocityEngine()).thenReturn(engine);
+        when(this.velocityManager.getVelocityEngine()).thenReturn(engine);
         when(engine.evaluate(any(VelocityContext.class), any(Writer.class), any(), eq("myCode"))).thenAnswer(
-                new Answer<Object>()
-                {
-                    @Override
-                    public Object answer(InvocationOnMock invocation) throws Throwable
-                    {
-                        // Get the writer
-                        Writer writer = (Writer) invocation.getArguments()[1];
-                        writer.write("Rendered code");
-                        return true;
-                    }
-                });
+            invocation -> {
+                // Get the writer
+                Writer writer = (Writer) invocation.getArguments()[1];
+                writer.write("Rendered code");
+                return true;
+            });
 
         // Test
-        assertEquals("Rendered code", mocker.getComponentUnderTest().render("myCode"));
+        assertEquals("Rendered code", this.velocityRenderer.render("myCode", null));
 
         // Verify
         verify(engine).startedUsingMacroNamespace("IconVelocityRenderer_" + Thread.currentThread().getId());
         verify(engine).stoppedUsingMacroNamespace("IconVelocityRenderer_" + Thread.currentThread().getId());
     }
 
     @Test
-    public void renderWithException() throws Exception
+    void renderWithException() throws Exception
     {
         //  Mocks
         Exception exception = new XWikiVelocityException("exception");
-        when(velocityManager.getVelocityEngine()).thenThrow(exception);
+        when(this.velocityManager.getVelocityEngine()).thenThrow(exception);
 
         // Test
         IconException caughtException = null;
         try {
-            mocker.getComponentUnderTest().render("myCode");
-        } catch(IconException e) {
+            this.velocityRenderer.render("myCode", null);
+        } catch (IconException e) {
             caughtException = e;
         }
 
         // Verify
         assertNotNull(caughtException);
         assertEquals("Failed to render the icon.", caughtException.getMessage());
         assertEquals(exception, caughtException.getCause());
     }
 
     @Test
-    public void renderWhenEvaluateReturnsFalse() throws Exception
+    void renderWhenEvaluateReturnsFalse() throws Exception
     {
         //  Mocks
         VelocityEngine engine = mock(VelocityEngine.class);
-        when(velocityManager.getVelocityEngine()).thenReturn(engine);
+        when(this.velocityManager.getVelocityEngine()).thenReturn(engine);
         when(engine.evaluate(any(VelocityContext.class), any(Writer.class), any(),
             eq("myCode"))).thenReturn(false);
 
         // Test
-        IconException caughtException = null;
-        try {
-            mocker.getComponentUnderTest().render("myCode");
-        } catch(IconException e) {
-            caughtException = e;
-        }
+        IconException caughtException = assertThrows(IconException.class,
+            () -> this.velocityRenderer.render("myCode", null));
 
         // Verify
-        assertNotNull(caughtException);
-        assertEquals("Failed to render the icon. See the Velocity runtime log.", caughtException.getMessage());
+        assertEquals("Failed to render the icon. See the Velocity runtime log.",
+            caughtException.getMessage());
 
         verify(engine).startedUsingMacroNamespace("IconVelocityRenderer_" + Thread.currentThread().getId());
         verify(engine).stoppedUsingMacroNamespace("IconVelocityRenderer_" + Thread.currentThread().getId());
     }
 
+    @Test
+    void renderWithContextDocument() throws Exception
+    {
+        // Mocks
+        VelocityEngine engine = mock(VelocityEngine.class);
+        when(this.velocityManager.getVelocityEngine()).thenReturn(engine);
+        when(engine.evaluate(any(VelocityContext.class), any(Writer.class), any(), eq("myCode"))).thenAnswer(
+            invocation -> {
+                // Get the writer
+                Writer writer = (Writer) invocation.getArguments()[1];
+                writer.write("Rendered code");
+                return true;
+            });
+
+        DocumentReference contextReference = new DocumentReference("xwiki", "Space", "IconTheme");
+        DocumentReference documentAuthorReference = new DocumentReference("xwiki", "XWiki", "User");
+        XWikiDocument document = mock(XWikiDocument.class);
+        UserReference authorReference = mock(UserReference.class);
+        DocumentAuthors documentAuthors = new DefaultDocumentAuthors(document);
+        documentAuthors.setContentAuthor(authorReference);
+        when(document.getAuthors()).thenReturn(documentAuthors);
+        when(this.documentUserSerializer.serialize(authorReference)).thenReturn(documentAuthorReference);
+        when(this.documentAccessBridge.getDocumentInstance(contextReference)).thenReturn(document);
+        when(this.authorExecutor.call(any(), eq(documentAuthorReference), eq(contextReference)))
+            .then(invocation -> invocation.getArgument(0, Callable.class).call());
+        when(this.documentContextExecutor.call(any(), eq(document)))
+            .then(invocation -> invocation.getArgument(0, Callable.class).call());
+
+        assertEquals("Rendered code", this.velocityRenderer.render("myCode", contextReference));
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-rest/xwiki-platform-icon-rest-default/src/test/java/org/xwiki/icon/internal/DefaultIconThemesResourceTest.java b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-rest/xwiki-platform-icon-rest-default/src/test/java/org/xwiki/icon/internal/DefaultIconThemesResourceTest.java
index c6821e1a448bf..a6127c0ae6f77 100644
--- a/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-rest/xwiki-platform-icon-rest-default/src/test/java/org/xwiki/icon/internal/DefaultIconThemesResourceTest.java
+++ b/xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-rest/xwiki-platform-icon-rest-default/src/test/java/org/xwiki/icon/internal/DefaultIconThemesResourceTest.java
@@ -227,11 +227,11 @@ void getIcons() throws Exception
     }
 
     @Test
     void getIconsIconManagerException() throws Exception
     {
-        IconException iconException = new IconException("icon error", null);
+        IconException iconException = new IconException("icon error");
 
         when(this.iconManager.hasIcon(any(), any())).thenThrow(iconException);
         when(this.iconSetManager.getCurrentIconSet()).thenReturn(new IconSet("testTheme"));
 
         List<String> names = asList("iconA", "unknownIcon", "iconB");
diff --git a/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/main/java/org/xwiki/rendering/internal/util/XWikiSyntaxEscaper.java b/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/main/java/org/xwiki/rendering/internal/util/XWikiSyntaxEscaper.java
new file mode 100644
index 0000000000000..2504e4c3f904d
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/main/java/org/xwiki/rendering/internal/util/XWikiSyntaxEscaper.java
@@ -0,0 +1,90 @@
+/*
+ * 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.rendering.internal.util;
+
+import javax.inject.Singleton;
+
+import org.xwiki.component.annotation.Component;
+import org.xwiki.rendering.syntax.Syntax;
+
+/**
+ * Escaping helpers.
+ *
+ * @version $Id$
+ * @since 14.10.6
+ * @since 15.2RC1
+ */
+@Component(roles = XWikiSyntaxEscaper.class)
+@Singleton
+public class XWikiSyntaxEscaper
+{
+    /**
+     * Escapes a give text using the escaping method specific to the given syntax.
+     * <p>
+     * One example of escaping method is using escape characters like {@code ~} for the {@link Syntax#XWIKI_2_1} syntax
+     * on all or just some characters of the given text.
+     * <p>
+     * The current implementation only escapes XWiki 1.0, 2.0 and 2.1 syntaxes.
+     *
+     * @param content the text to escape
+     * @param syntax the syntax to escape the content in (e.g. {@link Syntax#XWIKI_1_0}, {@link Syntax#XWIKI_2_0},
+     *     {@link Syntax#XWIKI_2_1}, etc.). This is the syntax where the output will be used and not necessarily the
+     *     same syntax of the input content
+     * @return the escaped text or {@code null} if the given content or the given syntax are {@code null}, or if the
+     *     syntax is not supported
+     */
+    public String escape(String content, Syntax syntax)
+    {
+        if (content == null || syntax == null) {
+            return null;
+        }
+
+        // Determine the escape character for the syntax.
+        char escapeChar;
+        try {
+            escapeChar = getEscapeCharacter(syntax);
+        } catch (Exception e) {
+            // We don`t know how to proceed, so we just return null.
+            return null;
+        }
+
+        // Since we prefix all characters, the result size will be double the input's, so we can just use char[].
+        char[] result = new char[content.length() * 2];
+
+        // Escape the content.
+        for (int i = 0; i < content.length(); i++) {
+            result[2 * i] = escapeChar;
+            result[2 * i + 1] = content.charAt(i);
+        }
+
+        return String.valueOf(result);
+    }
+
+    private char getEscapeCharacter(Syntax syntax) throws IllegalArgumentException
+    {
+        if (Syntax.XWIKI_1_0.equals(syntax)) {
+            return '\\';
+        } else if (Syntax.XWIKI_2_0.equals(syntax) || Syntax.XWIKI_2_1.equals(syntax)) {
+            return '~';
+        }
+
+        throw new IllegalArgumentException(String.format("Escaping is not supported for Syntax [%s]", syntax));
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/main/java/org/xwiki/rendering/script/RenderingScriptService.java b/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/main/java/org/xwiki/rendering/script/RenderingScriptService.java
index d425e6560bb12..da02fb9e4e1f6 100644
--- a/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/main/java/org/xwiki/rendering/script/RenderingScriptService.java
+++ b/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/main/java/org/xwiki/rendering/script/RenderingScriptService.java
@@ -36,10 +36,11 @@
 import org.xwiki.component.manager.ComponentManager;
 import org.xwiki.rendering.block.Block;
 import org.xwiki.rendering.block.XDOM;
 import org.xwiki.rendering.configuration.ExtendedRenderingConfiguration;
 import org.xwiki.rendering.configuration.RenderingConfiguration;
+import org.xwiki.rendering.internal.util.XWikiSyntaxEscaper;
 import org.xwiki.rendering.macro.MacroCategoryManager;
 import org.xwiki.rendering.macro.MacroId;
 import org.xwiki.rendering.macro.MacroIdFactory;
 import org.xwiki.rendering.macro.MacroLookupException;
 import org.xwiki.rendering.macro.MacroManager;
@@ -88,10 +89,13 @@ public class RenderingScriptService implements ScriptService
     private MacroCategoryManager macroCategoryManager;
     
     @Inject
     private MacroIdFactory macroIdFactory;
 
+    @Inject
+    private XWikiSyntaxEscaper escaper;
+
     /**
      * @return the list of syntaxes for which a Parser is available
      */
     public List<Syntax> getAvailableParserSyntaxes()
     {
@@ -213,33 +217,11 @@ public Syntax resolveSyntax(String syntaxId)
      *         syntax is not supported
      * @since 7.1M1
      */
     public String escape(String content, Syntax syntax)
     {
-        if (content == null || syntax == null) {
-            return null;
-        }
-
-        // Determine the escape character for the syntax.
-        char escapeChar;
-        try {
-            escapeChar = getEscapeCharacter(syntax);
-        } catch (Exception e) {
-            // We don`t know how to proceed, so we just return null.
-            return null;
-        }
-
-        // Since we prefix all characters, the result size will be double the input's, so we can just use char[].
-        char[] result = new char[content.length() * 2];
-
-        // Escape the content.
-        for (int i = 0; i < content.length(); i++) {
-            result[2 * i] = escapeChar;
-            result[2 * i + 1] = content.charAt(i);
-        }
-
-        return String.valueOf(result);
+        return this.escaper.escape(content, syntax);
     }
 
     /**
      * @return the list of Rendering Syntaxes that are configured for the current wiki (i.e. that are proposed to the
      *         user when editing wiki pages). These are input Syntaxes only (i.e. Syntaxes having a Parser for them).
@@ -328,17 +310,6 @@ public Set<String> getMacroCategories(MacroId macroId)
     @Unstable
     public Set<String> getHiddenMacroCategories()
     {
         return this.macroCategoryManager.getHiddenCategories();
     }
-
-    private char getEscapeCharacter(Syntax syntax) throws IllegalArgumentException
-    {
-        if (Syntax.XWIKI_1_0.equals(syntax)) {
-            return '\\';
-        } else if (Syntax.XWIKI_2_0.equals(syntax) || Syntax.XWIKI_2_1.equals(syntax)) {
-            return '~';
-        }
-
-        throw new IllegalArgumentException(String.format("Escaping is not supported for Syntax [%s]", syntax));
-    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/main/resources/META-INF/components.txt b/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/main/resources/META-INF/components.txt
index d740d4adf1ca0..8efe2f7fb1777 100644
--- a/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/main/resources/META-INF/components.txt
+++ b/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/main/resources/META-INF/components.txt
@@ -10,5 +10,6 @@ org.xwiki.rendering.internal.resolver.DocumentResourceReferenceEntityReferenceRe
 org.xwiki.rendering.internal.resolver.PageAttachmentResourceReferenceEntityReferenceResolver
 org.xwiki.rendering.internal.resolver.PageResourceReferenceEntityReferenceResolver
 org.xwiki.rendering.internal.resolver.SpaceResourceReferenceEntityReferenceResolver
 org.xwiki.rendering.script.RenderingScriptService
 org.xwiki.rendering.internal.parser.LinkParser
+org.xwiki.rendering.internal.util.XWikiSyntaxEscaper
diff --git a/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/test/java/org/xwiki/rendering/script/RenderingScriptServiceTest.java b/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/test/java/org/xwiki/rendering/script/RenderingScriptServiceTest.java
index b0f2a6b049f0b..b5d763281e812 100644
--- a/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/test/java/org/xwiki/rendering/script/RenderingScriptServiceTest.java
+++ b/xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/test/java/org/xwiki/rendering/script/RenderingScriptServiceTest.java
@@ -29,10 +29,11 @@
 import org.junit.jupiter.api.extension.RegisterExtension;
 import org.xwiki.component.internal.ContextComponentManagerProvider;
 import org.xwiki.rendering.block.XDOM;
 import org.xwiki.rendering.configuration.ExtendedRenderingConfiguration;
 import org.xwiki.rendering.configuration.RenderingConfiguration;
+import org.xwiki.rendering.internal.util.XWikiSyntaxEscaper;
 import org.xwiki.rendering.macro.Macro;
 import org.xwiki.rendering.macro.MacroCategoryManager;
 import org.xwiki.rendering.macro.MacroId;
 import org.xwiki.rendering.macro.MacroIdFactory;
 import org.xwiki.rendering.macro.MacroManager;
@@ -72,11 +73,11 @@
  *
  * @version $Id$
  * @since 3.2M3
  */
 @ComponentTest
-@ComponentList({ ContextComponentManagerProvider.class })
+@ComponentList({ ContextComponentManagerProvider.class, XWikiSyntaxEscaper.class })
 class RenderingScriptServiceTest
 {
     @InjectMockComponents
     private RenderingScriptService renderingScriptService;
 

Vulnerable code:     <xwiki.jacoco.instructionRatio>0.39</xwiki.jacoco.instructionRatio>
        return velocityRenderer.render(contentToParse.toString());
            throw new IconException("The icon set is null", null);
        String url = velocityRenderer.render(iconSet.getCss());
        Map<String, Object> parameters = new HashMap();
        linkExtension.use(url, parameters);
        skinExtension.use(iconSet.getSsx());
        jsExtension.use(iconSet.getJsx());
            DocumentModelBridge doc = documentAccessBridge.getDocumentInstance(iconSetReference);
            DocumentReference iconClassRef = new DocumentReference(wikiDescriptorManager.getCurrentWikiId(),
            String name = (String) documentAccessBridge.getProperty(iconSetReference, iconClassRef, "name");
            return loadIconSet(new StringReader(content), name);
        IconSet iconSet = iconSetCache.get(name, wikiDescriptorManager.getCurrentWikiId());
                Query query = queryManager.createQuery(xwql, Query.XWQL);
                List<String> results = query.execute();
                if (results.isEmpty()) {
                    return null;
                }
                // Get the first result
                String docName = results.get(0);
                DocumentReference docRef = documentReferenceResolver.resolve(docName);
                iconSet = iconSetLoader.loadIconSet(docRef);
                iconSetCache.put(docRef, iconSet);
                iconSetCache.put(name, wikiDescriptorManager.getCurrentWikiId(), iconSet);
            } catch (QueryException e) {
                throw new IconException(String.format("Failed to load the icon set [%s].", name), e);
        // Return the icon set
import org.xwiki.velocity.XWikiVelocityException;
    public String render(String code) throws IconException
            engine = velocityManager.getVelocityEngine();
            VelocityContext context = new XWikiVelocityContext(velocityManager.getVelocityContext(),
            if (engine.evaluate(context, output, "DefaultIconRenderer", code)) {
                return output.toString();
            } else {
                // I don't know how to check the velocity runtime log
                throw new IconException("Failed to render the icon. See the Velocity runtime log.", null);
        } catch (XWikiVelocityException e) {
        when(velocityRenderer.render("#set($icon = \"blabla\")\nimage:$icon.png")).thenReturn("image:blabla.png");
        when(velocityRenderer.render("css")).thenReturn("velocityParsedCSS");
        when(velocityRenderer.render("#set($icon = \"blabla\")\n<img src=\"$icon.png\" />"))
        when(velocityRenderer.render("css")).thenReturn("velocityParsedCSS");
        IconException exception = new IconException("exception", null);
        when(velocityRenderer.render(any())).thenThrow(exception);
        when(velocityRenderer.render("#set($icon = \"hello\")\nfa fa-$icon")).thenReturn("fa fa-hello");
import static org.mockito.ArgumentMatchers.eq;
public class DefaultIconSetLoaderTest
    public void setUp() throws Exception
        when(wikiDescriptorManager.getCurrentWikiId()).thenReturn("wikiId");
    public void loadIconSet() throws Exception
        IconSet result = iconSetLoader.loadIconSet(content, "FontAwesome");
    public void loadIconSetFromWikiDocument() throws Exception
        when(documentAccessBridge.getProperty(eq(iconSetRef), eq(iconClassRef), eq("name"))).thenReturn("MyIconTheme");
        when(documentAccessBridge.getDocumentInstance(iconSetRef)).thenReturn(doc);
        IconSet result = iconSetLoader.loadIconSet(iconSetRef);
    public void loadIconSetWithException() throws Exception
        Exception caughException = null;
        try {
            iconSetLoader.loadIconSet(content, "FontAwesome");
        } catch (IconException e) {
            caughException = e;
        }

        assertNotNull(caughException);
        assert (caughException instanceof IconException);
        assertEquals(exception, caughException.getCause());
        assertEquals("Failed to load the IconSet [FontAwesome].", caughException.getMessage());
    public void loadIconSetFromWikiDocumentWithException() throws Exception
        when(documentAccessBridge.getDocumentInstance(any(DocumentReference.class))).thenThrow(exception);
        // Test
        Exception caughException = null;
        try {
            iconSetLoader.loadIconSet(new DocumentReference("a", "b", "c"));
        } catch (IconException e) {
            caughException = e;
        }

        assertNotNull(caughException);
        assert (caughException instanceof IconException);
        assertEquals(exception, caughException.getCause());
        assertEquals("Failed to load the IconSet [a:b.c].", caughException.getMessage());
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.xwiki.test.mockito.MockitoComponentMockingRule;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
public class VelocityRendererTest
    @Rule
    public MockitoComponentMockingRule<VelocityRenderer> mocker =
            new MockitoComponentMockingRule<>(VelocityRenderer.class);

    @Before
    public void setUp() throws Exception
    {
        velocityManager = mocker.getInstance(VelocityManager.class);
    }
    public void renderTest() throws Exception
        when(velocityManager.getVelocityEngine()).thenReturn(engine);
                new Answer<Object>()
                {
                    @Override
                    public Object answer(InvocationOnMock invocation) throws Throwable
                    {
                        // Get the writer
                        Writer writer = (Writer) invocation.getArguments()[1];
                        writer.write("Rendered code");
                        return true;
                    }
                });
        assertEquals("Rendered code", mocker.getComponentUnderTest().render("myCode"));
    public void renderWithException() throws Exception
        when(velocityManager.getVelocityEngine()).thenThrow(exception);
            mocker.getComponentUnderTest().render("myCode");
        } catch(IconException e) {
    public void renderWhenEvaluateReturnsFalse() throws Exception
        when(velocityManager.getVelocityEngine()).thenReturn(engine);
        IconException caughtException = null;
        try {
            mocker.getComponentUnderTest().render("myCode");
        } catch(IconException e) {
            caughtException = e;
        }
        assertNotNull(caughtException);
        assertEquals("Failed to render the icon. See the Velocity runtime log.", caughtException.getMessage());
        IconException iconException = new IconException("icon error", null);
        if (content == null || syntax == null) {
            return null;
        }

        // Determine the escape character for the syntax.
        char escapeChar;
        try {
            escapeChar = getEscapeCharacter(syntax);
        } catch (Exception e) {
            // We don`t know how to proceed, so we just return null.
            return null;
        }

        // Since we prefix all characters, the result size will be double the input's, so we can just use char[].
        char[] result = new char[content.length() * 2];

        // Escape the content.
        for (int i = 0; i < content.length(); i++) {
            result[2 * i] = escapeChar;
            result[2 * i + 1] = content.charAt(i);
        }

        return String.valueOf(result);

    private char getEscapeCharacter(Syntax syntax) throws IllegalArgumentException
    {
        if (Syntax.XWIKI_1_0.equals(syntax)) {
            return '\\';
        } else if (Syntax.XWIKI_2_0.equals(syntax) || Syntax.XWIKI_2_1.equals(syntax)) {
            return '~';
        }

        throw new IllegalArgumentException(String.format("Escaping is not supported for Syntax [%s]", syntax));
    }
@ComponentList({ ContextComponentManagerProvider.class })
Fixed code:     <xwiki.jacoco.instructionRatio>0.50</xwiki.jacoco.instructionRatio>
/*
 * 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.bridge.internal;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.bridge.DocumentModelBridge;
import org.xwiki.component.annotation.Component;
import org.xwiki.model.ModelContext;
import org.xwiki.model.reference.EntityReference;

/**
 * Default implementation of {@link DocumentContextExecutor}.
 *
 * @version $Id$
 * @since 14.10.6
 * @since 15.2RC1
 */
@Component
@Singleton
public class DefaultDocumentContextExecutor implements DocumentContextExecutor
{
    @Inject
    private DocumentAccessBridge documentAccessBridge;

    @Inject
    private ModelContext modelContext;

    @Override
    public <V> V call(Callable<V> callable, DocumentModelBridge document) throws Exception
    {
        Map<String, Object> backupObjects = new HashMap<>();
        EntityReference currentWikiReference = this.modelContext.getCurrentEntityReference();
        boolean canPop = false;

        try {
            this.documentAccessBridge.pushDocumentInContext(backupObjects, document);
            canPop = true;
            // Make sure to synchronize the context wiki with the context document's wiki.
            this.modelContext.setCurrentEntityReference(document.getDocumentReference().getWikiReference());

            return callable.call();
        } finally {
            if (canPop) {
                this.documentAccessBridge.popDocumentFromContext(backupObjects);
                // Also restore the context wiki.
                this.modelContext.setCurrentEntityReference(currentWikiReference);
            }
        }
    }
}
/*
 * 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.bridge.internal;

import java.util.concurrent.Callable;

import org.xwiki.bridge.DocumentModelBridge;
import org.xwiki.component.annotation.Role;

/**
 * Executes a {@link Callable} with the given document in context.
 *
 * @version $Id$
 * @since 14.10.6
 * @since 15.2RC1
 */
@Role
public interface DocumentContextExecutor
{
    /**
     * Execute the passed {@link Callable} with the given document in context.
     *
     * @param callable the task to execute
     * @param document the document to put in context
     * @return computed result
     * @param <V> the result type of method {@code call}
     */
    <V> V call(Callable<V> callable, DocumentModelBridge document) throws Exception;
}
org.xwiki.bridge.internal.DefaultDocumentContextExecutor
/*
 * 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.bridge.internal;

import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.bridge.DocumentModelBridge;
import org.xwiki.model.EntityType;
import org.xwiki.model.ModelContext;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.EntityReference;
import org.xwiki.model.reference.WikiReference;
import org.xwiki.test.junit5.mockito.ComponentTest;
import org.xwiki.test.junit5.mockito.InjectMockComponents;
import org.xwiki.test.junit5.mockito.MockComponent;

import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;

/**
 * Unit test for {@link DefaultDocumentContextExecutor}.
 *
 * @version $Id$
 */
@ComponentTest
class DefaultDocumentContextExecutorTest
{
    private static final WikiReference TEST_WIKI_REFERENCE = new WikiReference("xwiki");

    private static final DocumentReference TEST_DOCUMENT_REFERENCE =
        new DocumentReference(TEST_WIKI_REFERENCE.getName(), "Space", "WebHome");

    private static final WikiReference INITIAL_WIKI_REFERENCE = new WikiReference("initial");

    @MockComponent
    private ModelContext modelContext;

    @MockComponent
    private DocumentAccessBridge documentAccessBridge;

    @InjectMockComponents
    private DefaultDocumentContextExecutor executor;

    @Mock
    private Callable<Object> mockCallable;

    @Mock
    private DocumentModelBridge documentModelBridge;

    private WikiReference modelWikiReference;

    @BeforeEach
    public void beforeEach()
    {
        when(this.documentModelBridge.getDocumentReference()).thenReturn(TEST_DOCUMENT_REFERENCE);

        // Mock actually storing the wiki reference
        this.modelWikiReference = INITIAL_WIKI_REFERENCE;
        when(this.modelContext.getCurrentEntityReference()).thenReturn(this.modelWikiReference);
        doAnswer(invocationOnMock -> {
            EntityReference entityWikiReference =
                invocationOnMock.getArgument(0, EntityReference.class).extractReference(EntityType.WIKI);
            this.modelWikiReference = new WikiReference(entityWikiReference.getName());
            return null;
        }).when(this.modelContext).setCurrentEntityReference(any(EntityReference.class));
    }

    @Test
    void call() throws Exception
    {
        String callResult = "callResult";
        when(this.mockCallable.call()).thenReturn(callResult);
        AtomicReference<Map<String, Object>> backupObjects = new AtomicReference<>();
        doAnswer(invocationOnMock -> {
            backupObjects.set(invocationOnMock.getArgument(0));
            return null;
        }).when(this.documentAccessBridge).pushDocumentInContext(any(), same(this.documentModelBridge));

        Object actualResult = this.executor.call(this.mockCallable, this.documentModelBridge);

        assertSame(callResult, actualResult);
        InOrder inOrder = inOrder(this.documentAccessBridge, this.modelContext);
        inOrder.verify(this.documentAccessBridge).pushDocumentInContext(any(), same(this.documentModelBridge));
        inOrder.verify(this.modelContext).setCurrentEntityReference(TEST_WIKI_REFERENCE);
        inOrder.verify(this.documentAccessBridge).popDocumentFromContext(backupObjects.get());
        inOrder.verify(this.modelContext).setCurrentEntityReference(INITIAL_WIKI_REFERENCE);
    }

    @Test
    void popWhenCallableThrows() throws Exception
    {
        Exception testException = new Exception("Callable failed");
        when(this.mockCallable.call()).thenThrow(testException);

        assertThrows(Exception.class, () -> this.executor.call(this.mockCallable, this.documentModelBridge));
        InOrder inOrder = inOrder(this.documentAccessBridge, this.modelContext);
        inOrder.verify(this.modelContext).setCurrentEntityReference(TEST_WIKI_REFERENCE);
        inOrder.verify(this.documentAccessBridge).popDocumentFromContext(any());
        inOrder.verify(this.modelContext).setCurrentEntityReference(INITIAL_WIKI_REFERENCE);
    }

    @Test
    void doNothingWhenPushThrows() throws Exception
    {
        Exception testException = new Exception("Test");
        doThrow(testException)
            .when(this.documentAccessBridge).pushDocumentInContext(any(), same(this.documentModelBridge));

        assertThrows(Exception.class, () -> this.executor.call(this.mockCallable, this.documentModelBridge));

        verifyNoInteractions(this.mockCallable);
        verify(this.modelContext, never()).setCurrentEntityReference(any());
    }
}

    /**
     * Constructor with just a message.
     *
     * @param message the message to store in the exception
     * @since 14.10.6
     * @since 15.2RC1
     */
    public IconException(String message)
    {
        super(message);
    }
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.stability.Unstable;

    private DocumentReference sourceDocumentReference;


    /**
     * @return the document reference of the source of the icon theme, may be {@code null} if the icon set hasn't
     * been loaded from a document
     * @since 14.10.6
     * @since 15.2RC1
     */
    @Unstable
    public DocumentReference getSourceDocumentReference()
    {
        return this.sourceDocumentReference;
    }

    /**
     * @param sourceDocumentReference the reference to the source of the icon theme
     * @since 14.10.6
     * @since 15.2RC1
     */
    @Unstable
    public void setSourceDocumentReference(DocumentReference sourceDocumentReference)
    {
        this.sourceDocumentReference = sourceDocumentReference;
    }
    <dependency>
      <groupId>org.xwiki.platform</groupId>
      <artifactId>xwiki-platform-user-api</artifactId>
      <version>${project.version}</version>
    </dependency>
        return this.velocityRenderer.render(contentToParse.toString(), iconSet.getSourceDocumentReference());
            throw new IconException("The icon set is null");
        String url = this.velocityRenderer.render(iconSet.getCss(), iconSet.getSourceDocumentReference());
        Map<String, Object> parameters = new HashMap<>();
        this.linkExtension.use(url, parameters);
        this.skinExtension.use(iconSet.getSsx());
        this.jsExtension.use(iconSet.getJsx());
import javax.inject.Named;
import org.xwiki.security.authorization.AuthorizationManager;
import org.xwiki.security.authorization.Right;
import org.xwiki.user.UserReferenceSerializer;
    @Inject
    private AuthorizationManager authorizationManager;

    @Inject
    @Named("document")
    private UserReferenceSerializer<DocumentReference> documentUserSerializer;

            DocumentModelBridge doc = this.documentAccessBridge.getDocumentInstance(iconSetReference);

            // Check that both the content (actual icon theme content) and the metadata author (icon theme object)
            // have script right.
            DocumentReference contentAuthor =
                this.documentUserSerializer.serialize(doc.getAuthors().getContentAuthor());
            this.authorizationManager.checkAccess(Right.SCRIPT, contentAuthor, iconSetReference);
            DocumentReference metadataAuthor =
                this.documentUserSerializer.serialize(doc.getAuthors().getEffectiveMetadataAuthor());
            this.authorizationManager.checkAccess(Right.SCRIPT, metadataAuthor, iconSetReference);

            DocumentReference iconClassRef = new DocumentReference(this.wikiDescriptorManager.getCurrentWikiId(),
            String name = (String) this.documentAccessBridge.getProperty(iconSetReference, iconClassRef, "name");
            IconSet result = loadIconSet(new StringReader(content), name);
            result.setSourceDocumentReference(iconSetReference);
            return result;
import java.util.ArrayList;
import org.slf4j.Logger;
import static org.apache.commons.lang3.exception.ExceptionUtils.getRootCauseMessage;

    @Inject
    private Logger logger;

        IconSet iconSet = this.iconSetCache.get(name, this.wikiDescriptorManager.getCurrentWikiId());
            List<String> results;

                Query query = this.queryManager.createQuery(xwql, Query.XWQL);
                results = query.execute();
            } catch (QueryException e) {
                throw new IconException(String.format("Failed to load the icon set [%s].", name), e);
            }

            iconSet = loadIconSetFromCandidateDocuments(name, results);
        }

        // Return the icon set
        return iconSet;
    }

    private IconSet loadIconSetFromCandidateDocuments(String name, List<String> candidateDocuments) throws IconException
    {
        List<IconException> iconExceptions = new ArrayList<>();
        IconSet iconSet = null;
        // Try all results to find the first one that loads successfully.
        for (String docName : candidateDocuments) {
            DocumentReference docRef = this.documentReferenceResolver.resolve(docName);
            try {
                iconSet = this.iconSetLoader.loadIconSet(docRef);
                this.iconSetCache.put(docRef, iconSet);
                this.iconSetCache.put(name, this.wikiDescriptorManager.getCurrentWikiId(), iconSet);

                break;
            } catch (IconException e) {
                // Store the exception first, maybe there is another icon theme with the same name that loads
                // successfully.
                iconExceptions.add(e);
            }
        }

        if (iconSet == null && !iconExceptions.isEmpty()) {
            if (iconExceptions.size() > 1) {
                iconExceptions.stream().skip(1)
                    .forEach(e -> this.logger.warn("Failed loading icon set [{}] from multiple matching "
                            + "documents, ignored this additional exception, reason: [{}].", name,
                        getRootCauseMessage(e)));
                throw new IconException(String.format("Failed to load the icon set [%s] from %d documents, "
                        + "reporting the first exception, see the log for additional errors.",
                    name, candidateDocuments.size()),
                    iconExceptions.get(0));
            } else {
                throw iconExceptions.get(0);
import java.util.concurrent.Callable;
import javax.inject.Named;
import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.bridge.DocumentModelBridge;
import org.xwiki.bridge.internal.DocumentContextExecutor;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.security.authorization.AuthorExecutor;
import org.xwiki.user.UserReferenceSerializer;
    private static final String NAMESPACE = "DefaultIconRenderer";

    @Inject
    private DocumentAccessBridge documentAccessBridge;

    @Inject
    private AuthorExecutor authorExecutor;

    @Inject
    private DocumentContextExecutor documentContextExecutor;

    @Inject
    @Named("document")
    private UserReferenceSerializer<DocumentReference> documentUserSerializer;

     * @param contextDocumentReference the reference of the context document
    public String render(String code, DocumentReference contextDocumentReference) throws IconException

        boolean result;

            engine = this.velocityManager.getVelocityEngine();
            DocumentReference authorReference;
            DocumentModelBridge sourceDocument;

            // Execute the Velocity code in an isolated execution context with the rights of its author when the icon
            // theme is from a document.
            if (contextDocumentReference != null) {
                sourceDocument =
                    this.documentAccessBridge.getDocumentInstance(contextDocumentReference);
                authorReference = this.documentUserSerializer.serialize(sourceDocument.getAuthors().getContentAuthor());
            } else {
                authorReference = null;
                sourceDocument = null;
            }

            VelocityContext context = new XWikiVelocityContext(this.velocityManager.getVelocityContext(),
            VelocityEngine finalEngine = engine;
            Callable<Boolean> callable = () -> finalEngine.evaluate(context, output, NAMESPACE, code);
            if (contextDocumentReference != null) {
                // Wrap the callable in a document context and author executor to ensure that the document is in
                // context and the Velocity code is executed with the author's rights.
                Callable<Boolean> innerCallable = callable;
                callable = () -> this.documentContextExecutor.call(
                    () -> this.authorExecutor.call(innerCallable, authorReference, contextDocumentReference),
                    sourceDocument);
            result = callable.call();
        } catch (Exception e) {

        if (result) {
            return output.toString();
        } else {
            // I don't know how to check the velocity runtime log
            throw new IconException("Failed to render the icon. See the Velocity runtime log.");
        }
        when(this.velocityRenderer.render("#set($icon = \"blabla\")\nimage:$icon.png", null))
            .thenReturn("image:blabla.png");
        when(this.velocityRenderer.render("css", null)).thenReturn("velocityParsedCSS");
        when(this.velocityRenderer.render("#set($icon = \"blabla\")\n<img src=\"$icon.png\" />", null))
        when(this.velocityRenderer.render("css", null)).thenReturn("velocityParsedCSS");
        IconException exception = new IconException("exception");
        when(this.velocityRenderer.render(any(), any())).thenThrow(exception);
        when(this.velocityRenderer.render("#set($icon = \"hello\")\nfa fa-$icon", null)).thenReturn("fa fa-hello");
import org.xwiki.model.internal.document.DefaultDocumentAuthors;
import com.xpn.xwiki.doc.XWikiDocument;

import static org.junit.jupiter.api.Assertions.assertThrows;
class DefaultIconSetLoaderTest
    void setUp()
        when(this.wikiDescriptorManager.getCurrentWikiId()).thenReturn("wikiId");
    void loadIconSet() throws Exception
        IconSet result = this.iconSetLoader.loadIconSet(content, "FontAwesome");
    void loadIconSetFromWikiDocument() throws Exception
        when(this.documentAccessBridge.getProperty(iconSetRef, iconClassRef, "name")).thenReturn("MyIconTheme");
        when(this.documentAccessBridge.getDocumentInstance(iconSetRef)).thenReturn(doc);
        when(doc.getAuthors()).thenReturn(new DefaultDocumentAuthors(new XWikiDocument(iconSetRef)));

        IconSet result = this.iconSetLoader.loadIconSet(iconSetRef);
    void loadIconSetWithException() throws Exception
        Exception caughtException = assertThrows(IconException.class, () ->
            this.iconSetLoader.loadIconSet(content, "FontAwesome"));

        assertEquals(exception, caughtException.getCause());
        assertEquals("Failed to load the IconSet [FontAwesome].", caughtException.getMessage());
    void loadIconSetFromWikiDocumentWithException() throws Exception
        when(this.documentAccessBridge.getDocumentInstance(any(DocumentReference.class))).thenThrow(exception);
        IconException caughtException = assertThrows(IconException.class, () ->
            this.iconSetLoader.loadIconSet(new DocumentReference("a", "b", "c")));

        assertEquals(exception, caughtException.getCause());
        assertEquals("Failed to load the IconSet [a:b.c].", caughtException.getMessage());
import org.junit.jupiter.api.extension.RegisterExtension;
import org.xwiki.test.LogLevel;
import org.xwiki.test.junit5.LogCaptureExtension;
import static org.junit.jupiter.api.Assertions.assertThrows;
    @RegisterExtension
    private LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);

    @Test
    void getIconSetWhenOneFails() throws Exception
    {
        // Mocks
        IconSet iconSet = new IconSet("silk");
        Query query = mock(Query.class);
        when(this.queryManager.createQuery("FROM doc.object(IconThemesCode.IconThemeClass) obj WHERE obj.name = :name",
            Query.XWQL)).thenReturn(query);
        List<String> results = List.of("FakeIcon.Silk", "IconThemes.Silk");
        when(query.<String>execute()).thenReturn(results);
        DocumentReference fakeDocumentReference = new DocumentReference("wiki", "FakeIcon", "Silk");
        when(this.documentReferenceResolver.resolve("FakeIcon.Silk")).thenReturn(fakeDocumentReference);
        when(this.iconSetLoader.loadIconSet(fakeDocumentReference)).thenThrow(new IconException("Test"));

        DocumentReference documentReference = new DocumentReference("wiki", "IconThemes", "Silk");
        when(this.documentReferenceResolver.resolve("IconThemes.Silk")).thenReturn(documentReference);
        when(this.iconSetLoader.loadIconSet(documentReference)).thenReturn(iconSet);

        // Test
        assertEquals(iconSet, this.iconSetManager.getIconSet("silk"));

        // Verify
        verify(query).bindValue("name", "silk");
        verify(this.iconSetCache).put(documentReference, iconSet);
        verify(this.iconSetCache).put("silk", "currentWikiId", iconSet);
        verify(this.iconSetLoader).loadIconSet(fakeDocumentReference);
    }

    @Test
    void getIconSetWhenAllFail() throws Exception
    {
        // Mocks
        Query query = mock(Query.class);
        when(this.queryManager.createQuery("FROM doc.object(IconThemesCode.IconThemeClass) obj WHERE obj.name = :name",
            Query.XWQL)).thenReturn(query);
        List<String> results = List.of("FakeIcon.Silk", "IconThemes.Silk");
        when(query.<String>execute()).thenReturn(results);
        DocumentReference fakeDocumentReference = new DocumentReference("wiki", "FakeIcon", "Silk");
        when(this.documentReferenceResolver.resolve("FakeIcon.Silk")).thenReturn(fakeDocumentReference);
        IconException fakeException = new IconException("Fake");
        when(this.iconSetLoader.loadIconSet(fakeDocumentReference)).thenThrow(fakeException);

        DocumentReference documentReference = new DocumentReference("wiki", "IconThemes", "Silk");
        when(this.documentReferenceResolver.resolve("IconThemes.Silk")).thenReturn(documentReference);
        when(this.iconSetLoader.loadIconSet(documentReference)).thenThrow(new IconException("Real"));

        // Test
        IconException exception = assertThrows(IconException.class, () -> this.iconSetManager.getIconSet("silk"));
        assertEquals("Failed to load the icon set [silk] from 2 documents, reporting the first exception, see the"
            + " log for additional errors.", exception.getMessage());
        assertEquals(fakeException, exception.getCause());

        assertEquals(1, this.logCapture.size());
        assertEquals("Failed loading icon set [silk] from multiple matching documents, "
                + "ignored this additional exception, reason: [IconException: Real].",
            this.logCapture.getMessage(0));

        // Verify
        verify(query).bindValue("name", "silk");
        verify(this.iconSetCache, never()).put(anyString(), any());
        verify(this.iconSetCache, never()).put(any(DocumentReference.class), any());
        verify(this.iconSetLoader).loadIconSet(fakeDocumentReference);
    }

import java.util.concurrent.Callable;

import javax.inject.Named;
import org.junit.jupiter.api.Test;
import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.bridge.internal.DocumentContextExecutor;
import org.xwiki.model.document.DocumentAuthors;
import org.xwiki.model.internal.document.DefaultDocumentAuthors;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.security.authorization.AuthorExecutor;
import org.xwiki.test.junit5.mockito.ComponentTest;
import org.xwiki.test.junit5.mockito.InjectMockComponents;
import org.xwiki.test.junit5.mockito.MockComponent;
import org.xwiki.user.UserReference;
import org.xwiki.user.UserReferenceSerializer;
import com.xpn.xwiki.doc.XWikiDocument;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
@ComponentTest
class VelocityRendererTest
    @MockComponent
    @MockComponent
    private DocumentAccessBridge documentAccessBridge;

    @MockComponent
    @Named("document")
    private UserReferenceSerializer<DocumentReference> documentUserSerializer;

    @MockComponent
    private AuthorExecutor authorExecutor;

    @MockComponent
    private DocumentContextExecutor documentContextExecutor;

    @InjectMockComponents
    private VelocityRenderer velocityRenderer;
    void renderTest() throws Exception
        when(this.velocityManager.getVelocityEngine()).thenReturn(engine);
            invocation -> {
                // Get the writer
                Writer writer = (Writer) invocation.getArguments()[1];
                writer.write("Rendered code");
                return true;
            });
        assertEquals("Rendered code", this.velocityRenderer.render("myCode", null));
    void renderWithException() throws Exception
        when(this.velocityManager.getVelocityEngine()).thenThrow(exception);
            this.velocityRenderer.render("myCode", null);
        } catch (IconException e) {
    void renderWhenEvaluateReturnsFalse() throws Exception
        when(this.velocityManager.getVelocityEngine()).thenReturn(engine);
        IconException caughtException = assertThrows(IconException.class,
            () -> this.velocityRenderer.render("myCode", null));
        assertEquals("Failed to render the icon. See the Velocity runtime log.",
            caughtException.getMessage());
    @Test
    void renderWithContextDocument() throws Exception
    {
        // Mocks
        VelocityEngine engine = mock(VelocityEngine.class);
        when(this.velocityManager.getVelocityEngine()).thenReturn(engine);
        when(engine.evaluate(any(VelocityContext.class), any(Writer.class), any(), eq("myCode"))).thenAnswer(
            invocation -> {
                // Get the writer
                Writer writer = (Writer) invocation.getArguments()[1];
                writer.write("Rendered code");
                return true;
            });

        DocumentReference contextReference = new DocumentReference("xwiki", "Space", "IconTheme");
        DocumentReference documentAuthorReference = new DocumentReference("xwiki", "XWiki", "User");
        XWikiDocument document = mock(XWikiDocument.class);
        UserReference authorReference = mock(UserReference.class);
        DocumentAuthors documentAuthors = new DefaultDocumentAuthors(document);
        documentAuthors.setContentAuthor(authorReference);
        when(document.getAuthors()).thenReturn(documentAuthors);
        when(this.documentUserSerializer.serialize(authorReference)).thenReturn(documentAuthorReference);
        when(this.documentAccessBridge.getDocumentInstance(contextReference)).thenReturn(document);
        when(this.authorExecutor.call(any(), eq(documentAuthorReference), eq(contextReference)))
            .then(invocation -> invocation.getArgument(0, Callable.class).call());
        when(this.documentContextExecutor.call(any(), eq(document)))
            .then(invocation -> invocation.getArgument(0, Callable.class).call());

        assertEquals("Rendered code", this.velocityRenderer.render("myCode", contextReference));
    }
        IconException iconException = new IconException("icon error");
/*
 * 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.rendering.internal.util;

import javax.inject.Singleton;

import org.xwiki.component.annotation.Component;
import org.xwiki.rendering.syntax.Syntax;

/**
 * Escaping helpers.
 *
 * @version $Id$
 * @since 14.10.6
 * @since 15.2RC1
 */
@Component(roles = XWikiSyntaxEscaper.class)
@Singleton
public class XWikiSyntaxEscaper
{
    /**
     * Escapes a give text using the escaping method specific to the given syntax.
     * <p>
     * One example of escaping method is using escape characters like {@code ~} for the {@link Syntax#XWIKI_2_1} syntax
     * on all or just some characters of the given text.
     * <p>
     * The current implementation only escapes XWiki 1.0, 2.0 and 2.1 syntaxes.
     *
     * @param content the text to escape
     * @param syntax the syntax to escape the content in (e.g. {@link Syntax#XWIKI_1_0}, {@link Syntax#XWIKI_2_0},
     *     {@link Syntax#XWIKI_2_1}, etc.). This is the syntax where the output will be used and not necessarily the
     *     same syntax of the input content
     * @return the escaped text or {@code null} if the given content or the given syntax are {@code null}, or if the
     *     syntax is not supported
     */
    public String escape(String content, Syntax syntax)
    {
        if (content == null || syntax == null) {
            return null;
        }

        // Determine the escape character for the syntax.
        char escapeChar;
        try {
            escapeChar = getEscapeCharacter(syntax);
        } catch (Exception e) {
            // We don`t know how to proceed, so we just return null.
            return null;
        }

        // Since we prefix all characters, the result size will be double the input's, so we can just use char[].
        char[] result = new char[content.length() * 2];

        // Escape the content.
        for (int i = 0; i < content.length(); i++) {
            result[2 * i] = escapeChar;
            result[2 * i + 1] = content.charAt(i);
        }

        return String.valueOf(result);
    }

    private char getEscapeCharacter(Syntax syntax) throws IllegalArgumentException
    {
        if (Syntax.XWIKI_1_0.equals(syntax)) {
            return '\\';
        } else if (Syntax.XWIKI_2_0.equals(syntax) || Syntax.XWIKI_2_1.equals(syntax)) {
            return '~';
        }

        throw new IllegalArgumentException(String.format("Escaping is not supported for Syntax [%s]", syntax));
    }
}
import org.xwiki.rendering.internal.util.XWikiSyntaxEscaper;
    @Inject
    private XWikiSyntaxEscaper escaper;

        return this.escaper.escape(content, syntax);
org.xwiki.rendering.internal.util.XWikiSyntaxEscaper
import org.xwiki.rendering.internal.util.XWikiSyntaxEscaper;
@ComponentList({ ContextComponentManagerProvider.class, XWikiSyntaxEscaper.class })