CVE-2023-36470 (published 2023-06-29T21:15Z)

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.\n

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

Weakness information:
cwe_id: CWE-74
cwe_name: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
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.
extended_description: Software or other automated logic has certain assumptions about what constitutes data and control respectively. It is the lack of verification of these assumptions for user-controlled input that leads to injection problems. Injection problems encompass a wide variety of issues -- all mitigated in very different ways and usually attempted in order to alter the control flow of the process. For this reason, the most effective way to discuss these weaknesses is to note the distinct features that classify them as injection weaknesses. The most important issue to note is that all injection problems share one thing in common -- i.e., they allow for the injection of control plane data into the user-controlled data plane. This means that the execution of the process may be altered by sending code in through legitimate data channels, using no other mechanism. While buffer overflows, and many other flaws, involve the use of some further issue to gain execution, injection problems need only for the data to be parsed.
         

Fix information:
repo_url: https://github.com/xwiki/xwiki-platform
commit_details: repo_url: https://github.com/xwiki/xwiki-platform
author: Michael Hamann
author_date: 2023-03-02 13:47:57+01:00
msg: XWIKI-20682: Introduce an icon macro

* Introduce the displayIcon macro that safely parses and executes the
  icon theme's wiki syntax in the context of its source document.
* Add tests.
repository: repo_url: https://github.com/xwiki/xwiki-platform
repo_name: xwiki/xwiki-platform
description: The XWiki platform
repo_language: Java
owner: xwiki
file_changes: filename: pom.xml
change_type: ModificationType.MODIFY
diff: @@ -34,6 +34,7 @@
   <modules>
     <module>xwiki-platform-icon-api</module>
     <module>xwiki-platform-icon-default</module>
+    <module>xwiki-platform-icon-macro</module>
     <module>xwiki-platform-icon-script</module>
     <module>xwiki-platform-icon-ui</module>
     <module>xwiki-platform-icon-rest</module>

programming_language: HTML
filename: pom.xml
change_type: ModificationType.ADD
diff: @@ -0,0 +1,114 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ * 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.
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.xwiki.platform</groupId>
+    <artifactId>xwiki-platform-icon</artifactId>
+    <version>15.2-SNAPSHOT</version>
+  </parent>
+  <artifactId>xwiki-platform-icon-macro</artifactId>
+  <name>XWiki Platform - Icon - Macro</name>
+  <description>Macro to display icons.</description>
+  <properties>
+    <xwiki.jacoco.instructionRatio>1.00</xwiki.jacoco.instructionRatio>
+    <!-- Name to display by the Extension Manager -->
+    <xwiki.extension.name>Icon Macro</xwiki.extension.name>
+    <xwiki.extension.category>macro</xwiki.extension.category>
+  </properties>
+  <dependencies>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-icon-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-bridge</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.xwiki.rendering</groupId>
+      <artifactId>xwiki-rendering-transformation-macro</artifactId>
+      <version>${rendering.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-model-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-rendering-async-macro</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.xwiki.rendering</groupId>
+      <artifactId>xwiki-rendering-test</artifactId>
+      <version>${rendering.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.xwiki.rendering</groupId>
+      <artifactId>xwiki-rendering-syntax-xwiki21</artifactId>
+      <version>${rendering.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.xwiki.rendering</groupId>
+      <artifactId>xwiki-rendering-syntax-event</artifactId>
+      <version>${rendering.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-icon-default</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>javax.servlet</groupId>
+      <artifactId>javax.servlet-api</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mortbay.jasper</groupId>
+      <artifactId>apache-el</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <!-- For UserReferenceSerializer -->
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-user-default</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-oldcore</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+      <type>test-jar</type>
+    </dependency>
+  </dependencies>
+</project>
\ No newline at end of file

programming_language: HTML
filename: DisplayIconMacroParameters.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,103 @@
+/*
+ * 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.icon.macro;
+
+import org.xwiki.icon.macro.internal.DisplayIconMacro;
+import org.xwiki.properties.annotation.PropertyAdvanced;
+import org.xwiki.properties.annotation.PropertyDescription;
+import org.xwiki.properties.annotation.PropertyMandatory;
+import org.xwiki.properties.annotation.PropertyName;
+import org.xwiki.stability.Unstable;
+
+/**
+ * Parameters for the {@link DisplayIconMacro} Macro.
+ *
+ * @version $Id$
+ * @since 14.10.6
+ * @since 15.2RC1
+ */
+@Unstable
+public class DisplayIconMacroParameters
+{
+    private String name;
+
+    private String iconSet;
+
+    private boolean fallback = true;
+
+    /**
+     * @return the name of the icon
+     */
+    public String getName()
+    {
+        return this.name;
+    }
+
+    /**
+     * @param name the name of the icon
+     */
+    @PropertyName("Name")
+    @PropertyDescription("The name of the icon.")
+    @PropertyMandatory
+    public void setName(String name)
+    {
+        this.name = name;
+    }
+
+    /**
+     * @return the name of the icon theme
+     */
+    public String getIconSet()
+    {
+        return this.iconSet;
+    }
+
+    /**
+     * @param iconSet the name of the icon theme
+     */
+    @PropertyName("Icon Set")
+    @PropertyDescription("The name of the icon set")
+    public void setIconSet(String iconSet)
+    {
+        this.iconSet = iconSet;
+    }
+
+    /**
+     * @return if the icon shall be loaded from the default icon set when the icon or icon set is not available, true
+     * by default
+     */
+    public boolean isFallback()
+    {
+        return this.fallback;
+    }
+
+    /**
+     * @param fallback if the icon shall be loaded from the default icon set when the icon or icon set is not available
+     * icon set
+     */
+    @PropertyName("Fallback")
+    @PropertyDescription("If the icon shall be loaded from the default icon set when the icon or icon set is not "
+        + "available, true by default")
+    @PropertyAdvanced
+    public void setFallback(boolean fallback)
+    {
+        this.fallback = fallback;
+    }
+}

programming_language: Java
filename: DisplayIconMacro.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,193 @@
+/*
+ * 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.icon.macro.internal;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+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.icon.IconRenderer;
+import org.xwiki.icon.IconSet;
+import org.xwiki.icon.IconSetManager;
+import org.xwiki.icon.macro.DisplayIconMacroParameters;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.model.reference.EntityReferenceSerializer;
+import org.xwiki.rendering.async.internal.AbstractExecutedContentMacro;
+import org.xwiki.rendering.async.internal.block.BlockAsyncRendererConfiguration;
+import org.xwiki.rendering.block.Block;
+import org.xwiki.rendering.block.XDOM;
+import org.xwiki.rendering.listener.MetaData;
+import org.xwiki.rendering.macro.MacroExecutionException;
+import org.xwiki.rendering.syntax.Syntax;
+import org.xwiki.rendering.transformation.MacroTransformationContext;
+import org.xwiki.security.authorization.ContextualAuthorizationManager;
+import org.xwiki.security.authorization.Right;
+import org.xwiki.user.UserReferenceSerializer;
+
+/**
+ * Macro for displaying an icon.
+ *
+ * @version $Id$
+ * @since 14.10.6
+ * @since 15.2RC1
+ */
+@Component
+@Named("displayIcon")
+@Singleton
+public class DisplayIconMacro extends AbstractExecutedContentMacro<DisplayIconMacroParameters>
+{
+    private static final String DESCRIPTION = "Display an icon.";
+
+    @Inject
+    private IconSetManager iconSetManager;
+
+    @Inject
+    private IconRenderer iconRenderer;
+
+    @Inject
+    private EntityReferenceSerializer<String> defaultEntityReferenceSerializer;
+
+    @Inject
+    private ContextualAuthorizationManager contextualAuthorization;
+
+    @Inject
+    @Named("document")
+    private UserReferenceSerializer<DocumentReference> documentUserSerializer;
+
+    @Inject
+    private DocumentContextExecutor documentContextExecutor;
+
+    /**
+     * Default constructor.
+     */
+    public DisplayIconMacro()
+    {
+        super("Icon", DESCRIPTION, null, DisplayIconMacroParameters.class);
+
+        setDefaultCategories(Set.of(DEFAULT_CATEGORY_CONTENT));
+    }
+
+    @Override
+    public List<Block> execute(DisplayIconMacroParameters parameters, String content,
+        MacroTransformationContext context) throws MacroExecutionException
+    {
+        List<Block> result;
+
+        try {
+            IconSet iconSet = getIconSet(parameters);
+
+            if (iconSet == null) {
+                result = List.of();
+            } else {
+                XDOM iconBlock = parseIcon(parameters, context, iconSet);
+
+                BlockAsyncRendererConfiguration rendererConfiguration =
+                    createBlockAsyncRendererConfiguration(null, iconBlock, null, context);
+                rendererConfiguration.setAsyncAllowed(false);
+                rendererConfiguration.setCacheAllowed(false);
+
+                if (iconSet.getSourceDocumentReference() != null) {
+                    DocumentReference sourceDocumentReference = iconSet.getSourceDocumentReference();
+
+                    DocumentModelBridge sourceDocument =
+                        this.documentAccessBridge.getDocumentInstance(sourceDocumentReference);
+                    DocumentReference authorReference =
+                        this.documentUserSerializer.serialize(sourceDocument.getAuthors().getContentAuthor());
+
+                    rendererConfiguration.setSecureReference(sourceDocumentReference, authorReference);
+                    rendererConfiguration.useEntity(sourceDocumentReference);
+
+                    String stringDocumentReference =
+                        this.defaultEntityReferenceSerializer.serialize(iconSet.getSourceDocumentReference());
+                    rendererConfiguration.setTransformationId(stringDocumentReference);
+                    rendererConfiguration.setResricted(false);
+
+                    result = this.documentContextExecutor.call(
+                        () -> List.of(this.executor.execute(rendererConfiguration)),
+                        sourceDocument
+                    );
+                } else {
+                    result = List.of(this.executor.execute(rendererConfiguration));
+                }
+            }
+        } catch (MacroExecutionException e) {
+            throw e;
+        } catch (Exception e) {
+            throw new MacroExecutionException("Failed parsing and executing the icon.", e);
+        }
+
+        return result;
+    }
+
+    private XDOM parseIcon(DisplayIconMacroParameters parameters, MacroTransformationContext context, IconSet iconSet)
+        throws IconException, MacroExecutionException
+    {
+        String iconContent = this.iconRenderer.render(parameters.getName(), iconSet);
+        MetaData metaData = null;
+
+        if (iconSet.getSourceDocumentReference() != null) {
+            String stringReference =
+                this.defaultEntityReferenceSerializer.serialize(iconSet.getSourceDocumentReference());
+            metaData = new MetaData(Map.of(MetaData.SOURCE, stringReference));
+        }
+
+        return this.parser.parse(iconContent, Syntax.XWIKI_2_1, context, false, metaData,
+            context.isInline());
+    }
+
+    private IconSet getIconSet(DisplayIconMacroParameters parameters) throws IconException, MacroExecutionException
+    {
+        IconSet iconSet;
+
+        if (parameters.getIconSet() == null) {
+            iconSet = this.iconSetManager.getCurrentIconSet();
+        } else {
+            iconSet = this.iconSetManager.getIconSet(parameters.getIconSet());
+        }
+
+        // Check if the current user can access the icon theme. If not, fall back to the default icon theme or throw
+        // an exception when the fallback is disabled.
+        if (iconSet != null && iconSet.getSourceDocumentReference() != null
+            && !this.contextualAuthorization.hasAccess(Right.VIEW, iconSet.getSourceDocumentReference()))
+        {
+            if (parameters.isFallback()) {
+                iconSet = null;
+            } else {
+                throw new MacroExecutionException(
+                    String.format("Current user [%s] doesn't have view rights on the icon set's document [%s]",
+                        this.documentAccessBridge.getCurrentUserReference(), iconSet.getSourceDocumentReference()));
+            }
+        }
+
+        if (parameters.isFallback() && (iconSet == null || !iconSet.hasIcon(parameters.getName()))) {
+            iconSet = this.iconSetManager.getDefaultIconSet();
+        }
+
+        return iconSet;
+    }
+}

programming_language: Java
filename: ApplicationResources.properties
change_type: ModificationType.ADD
diff: @@ -0,0 +1,29 @@
+# ---------------------------------------------------------------------------
+# 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.
+# ---------------------------------------------------------------------------
+rendering.macro.displayIcon.name=Icon
+rendering.macro.displayIcon.description=Display an icon.
+rendering.macro.displayIcon.parameter.name.name=Name
+rendering.macro.displayIcon.parameter.name.description=The name of the icon.
+rendering.macro.displayIcon.parameter.iconSet.name=Icon Set
+rendering.macro.displayIcon.parameter.iconSet.description=The name of the icon set.
+rendering.macro.displayIcon.parameter.fallback.name=Fallback
+rendering.macro.displayIcon.parameter.fallback.description=If the icon shall be loaded from the default icon set when \
+  the icon or icon set is not available, true by default.
+

programming_language: Ruby
filename: components.txt
change_type: ModificationType.ADD
diff: @@ -0,0 +1 @@
+org.xwiki.icon.macro.internal.DisplayIconMacro

programming_language: SQL
filename: IntegrationTests.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,124 @@
+/*
+ * 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.icon.macro;
+
+import org.xwiki.bridge.DocumentAccessBridge;
+import org.xwiki.icon.Icon;
+import org.xwiki.icon.IconException;
+import org.xwiki.icon.IconSet;
+import org.xwiki.icon.IconSetCache;
+import org.xwiki.icon.IconSetManager;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.observation.ObservationManager;
+import org.xwiki.rendering.test.integration.junit5.RenderingTests;
+import org.xwiki.script.ScriptContextInitializer;
+import org.xwiki.security.authorization.AuthorizationManager;
+import org.xwiki.security.authorization.ContextualAuthorizationManager;
+import org.xwiki.security.authorization.Right;
+import org.xwiki.skin.SkinManager;
+import org.xwiki.skinx.SkinExtension;
+import org.xwiki.test.annotation.AllComponents;
+import org.xwiki.test.mockito.MockitoComponentManager;
+
+import com.xpn.xwiki.doc.XWikiDocument;
+
+import static org.mockito.Mockito.when;
+
+/**
+ * Run all tests found in {@code *.test} files located in the classpath. These {@code *.test} files must follow the
+ * conventions described in {@link org.xwiki.rendering.test.integration.TestDataParser}.
+ *
+ * @version $Id$
+ */
+@AllComponents
+public class IntegrationTests implements RenderingTests
+{
+    private static final DocumentReference ICON_DOCUMENT_REFERENCE = new DocumentReference("xwiki", "Icon", "Document");
+
+    /**
+     * Initializes various mocks to prevent errors in the integration tests.
+     *
+     * @param componentManager the component manager of the tests
+     * @throws Exception when the initialization fails
+     */
+    @RenderingTests.Initialized
+    public void initialize(MockitoComponentManager componentManager) throws Exception
+    {
+        // Mock the authorization managers as they try initializing a cache which fails (infinispan is not available
+        // in rendering tests).
+        ContextualAuthorizationManager authorizationManager =
+            componentManager.registerMockComponent(ContextualAuthorizationManager.class);
+        // Grant view right on the icon document.
+        when(authorizationManager.hasAccess(Right.VIEW, ICON_DOCUMENT_REFERENCE)).thenReturn(true);
+        componentManager.registerMockComponent(AuthorizationManager.class);
+
+        // Mock the icon set cache as it fails.
+        componentManager.registerMockComponent(IconSetCache.class);
+
+        // Mock skin extensions.
+        componentManager.registerMockComponent(SkinExtension.class, "ssx");
+        componentManager.registerMockComponent(SkinExtension.class, "jsx");
+        componentManager.registerMockComponent(SkinExtension.class, "linkx");
+
+        // Mock various components for the script context initialization.
+        componentManager.registerMockComponent(SkinManager.class);
+        componentManager.registerMockComponent(ScriptContextInitializer.class, "xwiki");
+        componentManager.registerMockComponent(ObservationManager.class);
+
+        DocumentAccessBridge documentAccessBridge = componentManager.registerMockComponent(DocumentAccessBridge.class);
+        XWikiDocument testDocument = new XWikiDocument(ICON_DOCUMENT_REFERENCE);
+        when(documentAccessBridge.getDocumentInstance(ICON_DOCUMENT_REFERENCE)).thenReturn(testDocument);
+
+        // Mock the icon set manager as we're not in a real enviornment where icon sets can be loaded.
+        IconSetManager iconSetManager = componentManager.registerMockComponent(IconSetManager.class);
+        setupIconThemes(iconSetManager);
+    }
+
+    private void setupIconThemes(IconSetManager iconSetManager) throws IconException
+    {
+        // The current icon set, the test icon set.
+        IconSet iconSet = new IconSet("test");
+        iconSet.addIcon("home", new Icon("homeIcon"));
+        iconSet.addIcon("page", new Icon("pageIcon"));
+        iconSet.setRenderWiki("(% class=\"icon\" data-xwiki-icon=\"$icon\" %)i(%%)");
+        when(iconSetManager.getCurrentIconSet()).thenReturn(iconSet);
+
+        // A special icon set to test loading icons from a different icon set.
+        IconSet specialSet = new IconSet("special");
+        specialSet.addIcon("home", new Icon("home"));
+        specialSet.setRenderWiki("special $icon");
+        when(iconSetManager.getIconSet("special")).thenReturn(specialSet);
+
+        // A document-based icon set to test executing in the context of a document.
+        IconSet documentIconSet = new IconSet("document");
+        documentIconSet.addIcon("document", new Icon("executed"));
+        documentIconSet.setRenderWiki("document $icon");
+        documentIconSet.setSourceDocumentReference(new DocumentReference(ICON_DOCUMENT_REFERENCE));
+        when(iconSetManager.getIconSet("document")).thenReturn(documentIconSet);
+
+        // The default icon set to test fallback to the default when the current or specified icon set doesn't
+        // contain an icon.
+        IconSet defaultSet = new IconSet("default");
+        defaultSet.addIcon("fallback", new Icon("fallbackIcon"));
+        defaultSet.setRenderWiki("fallback $icon");
+        when(iconSetManager.getDefaultIconSet()).thenReturn(defaultSet);
+    }
+}

programming_language: Java
filename: DisplayIconMacroTest.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,200 @@
+/*
+ * 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.icon.macro.internal;
+
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import javax.inject.Named;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.xwiki.bridge.DocumentAccessBridge;
+import org.xwiki.bridge.internal.DocumentContextExecutor;
+import org.xwiki.icon.Icon;
+import org.xwiki.icon.IconException;
+import org.xwiki.icon.IconRenderer;
+import org.xwiki.icon.IconSet;
+import org.xwiki.icon.IconSetManager;
+import org.xwiki.icon.macro.DisplayIconMacroParameters;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.model.reference.EntityReferenceSerializer;
+import org.xwiki.rendering.async.internal.block.BlockAsyncRendererConfiguration;
+import org.xwiki.rendering.async.internal.block.BlockAsyncRendererExecutor;
+import org.xwiki.rendering.block.Block;
+import org.xwiki.rendering.block.MetaDataBlock;
+import org.xwiki.rendering.block.WordBlock;
+import org.xwiki.rendering.block.XDOM;
+import org.xwiki.rendering.macro.MacroContentParser;
+import org.xwiki.rendering.macro.MacroExecutionException;
+import org.xwiki.rendering.syntax.Syntax;
+import org.xwiki.rendering.transformation.MacroTransformationContext;
+import org.xwiki.security.authorization.ContextualAuthorizationManager;
+import org.xwiki.security.authorization.Right;
+import org.xwiki.test.junit5.mockito.InjectMockComponents;
+import org.xwiki.test.junit5.mockito.MockComponent;
+import org.xwiki.user.UserReferenceSerializer;
+
+import com.xpn.xwiki.doc.XWikiDocument;
+import com.xpn.xwiki.test.MockitoOldcore;
+import com.xpn.xwiki.test.junit5.mockito.OldcoreTest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+/**
+ * Unit test for {@link DisplayIconMacro}.
+ *
+ * @version $Id$
+ */
+@OldcoreTest
+class DisplayIconMacroTest
+{
+    private static final DocumentReference ICON_DOCUMENT_REFERENCE = new DocumentReference("xwiki", "Icon", "Document");
+
+    private static final DocumentReference AUTHOR = new DocumentReference("xwiki", "XWiki", "Author");
+
+    @MockComponent
+    private ContextualAuthorizationManager contextualAuthorizationManager;
+
+    @MockComponent
+    private IconSetManager iconSetManager;
+
+    @MockComponent
+    private IconRenderer iconRenderer;
+
+    @MockComponent
+    private MacroContentParser macroContentParser;
+
+    @MockComponent
+    private DocumentContextExecutor documentContextExecutor;
+
+    @MockComponent
+    private BlockAsyncRendererExecutor blockAsyncRendererExecutor;
+
+    @MockComponent
+    private EntityReferenceSerializer<String> defaultEntityReferenceSerializer;
+
+    @MockComponent
+    @Named("document")
+    private UserReferenceSerializer<DocumentReference> documentUserSerializer;
+
+    @MockComponent
+    private DocumentAccessBridge documentAccessBridge;
+
+    @InjectMockComponents
+    private DisplayIconMacro displayIconMacro;
+
+    private final DisplayIconMacroParameters displayIconMacroParameters = new DisplayIconMacroParameters();
+
+    private XWikiDocument iconDocument;
+
+    @BeforeEach
+    public void before(MockitoOldcore oldcore) throws Exception
+    {
+        this.iconDocument = new XWikiDocument(ICON_DOCUMENT_REFERENCE);
+        this.iconDocument.setContentAuthorReference(AUTHOR);
+        oldcore.getSpyXWiki().saveDocument(this.iconDocument, oldcore.getXWikiContext());
+
+        IconSet documentIconSet = new IconSet("document");
+        documentIconSet.addIcon("home", new Icon("homeIcon"));
+        documentIconSet.setRenderWiki("icon $icon context {{contextDocumentAuthor /}}");
+        documentIconSet.setSourceDocumentReference(ICON_DOCUMENT_REFERENCE);
+        when(this.iconSetManager.getIconSet("document")).thenReturn(documentIconSet);
+
+        this.displayIconMacroParameters.setName("home");
+        this.displayIconMacroParameters.setIconSet("document");
+
+        when(this.iconRenderer.render(anyString(), any(IconSet.class)))
+            .then(invocation -> invocation.getArgument(0, String.class));
+        when(this.macroContentParser.parse(anyString(), eq(Syntax.XWIKI_2_1), any(), eq(false), any(), anyBoolean()))
+            .then(invocation -> new XDOM(List.of(new WordBlock(invocation.getArgument(0)))));
+        when(this.documentContextExecutor.call(any(), any()))
+            .then(invocation -> invocation.getArgument(0, Callable.class).call());
+        when(this.blockAsyncRendererExecutor.execute(any())).then(invocation -> invocation.getArgument(0,
+            BlockAsyncRendererConfiguration.class).getBlock());
+        when(this.defaultEntityReferenceSerializer.serialize(ICON_DOCUMENT_REFERENCE))
+            .thenReturn("xwiki:Icon.Document");
+        when(this.documentUserSerializer.serialize(any())).thenReturn(AUTHOR);
+        when(this.documentAccessBridge.getDocumentInstance(ICON_DOCUMENT_REFERENCE)).thenReturn(this.iconDocument);
+    }
+
+    @Test
+    void accessDenied()
+    {
+        when(this.contextualAuthorizationManager.hasAccess(Right.VIEW, ICON_DOCUMENT_REFERENCE)).thenReturn(false);
+        this.displayIconMacroParameters.setFallback(false);
+
+        MacroExecutionException executionException = assertThrows(MacroExecutionException.class,
+            () -> this.displayIconMacro.execute(this.displayIconMacroParameters, null,
+                mock(MacroTransformationContext.class)));
+        assertEquals(String.format("Current user [%s] doesn't have view rights on the icon set's document [%s]", null,
+            ICON_DOCUMENT_REFERENCE), executionException.getMessage());
+    }
+
+    @Test
+    void fallbackWhenAccessDenied() throws MacroExecutionException, IconException
+    {
+        when(this.contextualAuthorizationManager.hasAccess(Right.VIEW, ICON_DOCUMENT_REFERENCE)).thenReturn(false);
+        IconSet defaultIconSet = mock(IconSet.class);
+        when(this.iconSetManager.getDefaultIconSet()).thenReturn(defaultIconSet);
+
+        List<Block> result =
+            this.displayIconMacro.execute(this.displayIconMacroParameters, null, new MacroTransformationContext());
+        assertEquals(result, List.of(new MetaDataBlock(List.of(new WordBlock("home")))));
+        verify(this.iconRenderer).render("home", defaultIconSet);
+        verifyNoInteractions(this.documentContextExecutor);
+    }
+
+    @Test
+    void throwsWhenRenderingIconFails() throws IconException
+    {
+        when(this.contextualAuthorizationManager.hasAccess(Right.VIEW, ICON_DOCUMENT_REFERENCE)).thenReturn(true);
+
+        IconException testException = new IconException("Test");
+        when(this.iconRenderer.render("home", this.iconSetManager.getIconSet("document"))).thenThrow(testException);
+
+        MacroExecutionException result = assertThrows(MacroExecutionException.class, () ->
+            this.displayIconMacro.execute(this.displayIconMacroParameters, null, new MacroTransformationContext()));
+
+        assertEquals("Failed parsing and executing the icon.", result.getMessage());
+        assertSame(testException, result.getCause());
+    }
+
+    @Test
+    void executesInContext() throws Exception
+    {
+        when(this.contextualAuthorizationManager.hasAccess(Right.VIEW, ICON_DOCUMENT_REFERENCE)).thenReturn(true);
+
+        List<Block> result =
+            this.displayIconMacro.execute(this.displayIconMacroParameters, null, new MacroTransformationContext());
+        assertEquals(result, List.of(new MetaDataBlock(List.of(new WordBlock("home")))));
+        verify(this.documentContextExecutor).call(any(), eq(this.iconDocument));
+    }
+}

programming_language: Java
filename: macrodisplayicon1.test
change_type: ModificationType.ADD
diff: @@ -0,0 +1,36 @@
+.runTransformations
+.#-----------------------------------------------------
+.input|xwiki/2.1
+.# Verify the icon macro basic functionality
+.#-----------------------------------------------------
+Home icon: {{displayIcon name="home" /}}
+
+{{displayIcon name="page" /}}
+.#-----------------------------------------------------
+.expect|event/1.0
+.#-----------------------------------------------------
+beginDocument
+beginParagraph
+onWord [Home]
+onSpace
+onWord [icon]
+onSpecialSymbol [:]
+onSpace
+beginMacroMarkerInline [displayIcon] [name=home]
+beginMetaData [[syntax]=[XWiki 2.1]]
+beginFormat [NONE] [[class]=[icon][data-xwiki-icon]=[homeIcon]]
+onWord [i]
+endFormat [NONE] [[class]=[icon][data-xwiki-icon]=[homeIcon]]
+endMetaData [[syntax]=[XWiki 2.1]]
+endMacroMarkerInline [displayIcon] [name=home]
+endParagraph
+beginMacroMarkerStandalone [displayIcon] [name=page]
+beginMetaData [[syntax]=[XWiki 2.1]]
+beginParagraph
+beginFormat [NONE] [[class]=[icon][data-xwiki-icon]=[pageIcon]]
+onWord [i]
+endFormat [NONE] [[class]=[icon][data-xwiki-icon]=[pageIcon]]
+endParagraph
+endMetaData [[syntax]=[XWiki 2.1]]
+endMacroMarkerStandalone [displayIcon] [name=page]
+endDocument
\ No newline at end of file

programming_language: Lua
filename: macrodisplayicon2.test
change_type: ModificationType.ADD
diff: @@ -0,0 +1,20 @@
+.runTransformations
+.#-----------------------------------------------------
+.input|xwiki/2.1
+.# Verify that the icon set can be specified.
+.#-----------------------------------------------------
+{{displayIcon name="home" iconSet="special" /}}
+.#-----------------------------------------------------
+.expect|event/1.0
+.#-----------------------------------------------------
+beginDocument
+beginMacroMarkerStandalone [displayIcon] [name=home|iconSet=special]
+beginMetaData [[syntax]=[XWiki 2.1]]
+beginParagraph
+onWord [special]
+onSpace
+onWord [home]
+endParagraph
+endMetaData [[syntax]=[XWiki 2.1]]
+endMacroMarkerStandalone [displayIcon] [name=home|iconSet=special]
+endDocument
\ No newline at end of file

programming_language: Lua
filename: macrodisplayicon3.test
change_type: ModificationType.ADD
diff: @@ -0,0 +1,37 @@
+.runTransformations
+.#-----------------------------------------------------
+.input|xwiki/2.1
+.# Verify that the fallback to the default works and can be disabled.
+.#-----------------------------------------------------
+{{displayIcon name="fallback" iconSet="special" /}} {{displayIcon name="fallback" /}} {{displayIcon name="fallback" fallback="false" /}}
+
+{{displayIcon name="home" iconSet="none" fallback="false" /}}
+.#-----------------------------------------------------
+.expect|event/1.0
+.#-----------------------------------------------------
+beginDocument
+beginParagraph
+beginMacroMarkerInline [displayIcon] [name=fallback|iconSet=special]
+beginMetaData [[syntax]=[XWiki 2.1]]
+onWord [fallback]
+onSpace
+onWord [fallbackIcon]
+endMetaData [[syntax]=[XWiki 2.1]]
+endMacroMarkerInline [displayIcon] [name=fallback|iconSet=special]
+onSpace
+beginMacroMarkerInline [displayIcon] [name=fallback]
+beginMetaData [[syntax]=[XWiki 2.1]]
+onWord [fallback]
+onSpace
+onWord [fallbackIcon]
+endMetaData [[syntax]=[XWiki 2.1]]
+endMacroMarkerInline [displayIcon] [name=fallback]
+onSpace
+beginMacroMarkerInline [displayIcon] [name=fallback|fallback=false]
+beginMetaData
+endMetaData
+endMacroMarkerInline [displayIcon] [name=fallback|fallback=false]
+endParagraph
+beginMacroMarkerStandalone [displayIcon] [name=home|iconSet=none|fallback=false]
+endMacroMarkerStandalone [displayIcon] [name=home|iconSet=none|fallback=false]
+endDocument
\ No newline at end of file

programming_language: Lua
filename: macrodisplayicon4.test
change_type: ModificationType.ADD
diff: @@ -0,0 +1,20 @@
+.runTransformations
+.#-----------------------------------------------------
+.input|xwiki/2.1
+.# Verify the document-based icon set is rendered in document context
+.#-----------------------------------------------------
+{{displayIcon name="document" iconSet="document" /}}
+.#-----------------------------------------------------
+.expect|event/1.0
+.#-----------------------------------------------------
+beginDocument
+beginMacroMarkerStandalone [displayIcon] [name=document|iconSet=document]
+beginMetaData [[source]=[xwiki:Icon.Document][syntax]=[XWiki 2.1]]
+beginParagraph
+onWord [document]
+onSpace
+onWord [executed]
+endParagraph
+endMetaData [[source]=[xwiki:Icon.Document][syntax]=[XWiki 2.1]]
+endMacroMarkerStandalone [displayIcon] [name=document|iconSet=document]
+endDocument
\ No newline at end of file

programming_language: Lua
repo_url: https://github.com/xwiki/xwiki-platform
commit_details: repo_url: https://github.com/xwiki/xwiki-platform
author: Michael Hamann
author_date: 2023-03-02 13:49:44+01:00
msg: XWIKI-20524: Improve icon rendering

* Use the icon macro for rendering icons in the icon script service.
* Properly send the JSON response in the icon picker.
* Adapt the IconPickerMacro to not use the icon service rendering for
  resource loading but to use the dedicated method for that as the old
  way no longer works.
repository: repo_url: https://github.com/xwiki/xwiki-platform
repo_name: xwiki/xwiki-platform
description: The XWiki platform
repo_language: Java
owner: xwiki
file_changes: filename: pom.xml
change_type: ModificationType.MODIFY
diff: @@ -39,10 +39,21 @@
       <artifactId>xwiki-platform-icon-api</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-rendering-xwiki</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.xwiki.commons</groupId>
       <artifactId>xwiki-commons-script</artifactId>
       <version>${commons.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.xwiki.platform</groupId>
+      <artifactId>xwiki-platform-icon-macro</artifactId>
+      <version>${project.version}</version>
+      <scope>runtime</scope>
+    </dependency>
   </dependencies>
 </project>
\ No newline at end of file

programming_language: HTML
filename: IconManagerScriptService.java
change_type: ModificationType.MODIFY
diff: @@ -28,6 +28,8 @@
 
 import org.xwiki.component.annotation.Component;
 import org.xwiki.context.Execution;
+import org.xwiki.rendering.internal.util.XWikiSyntaxEscaper;
+import org.xwiki.rendering.syntax.Syntax;
 import org.xwiki.script.service.ScriptService;
 
 /**
@@ -58,6 +60,9 @@ public class IconManagerScriptService implements ScriptService
     @Inject
     private Execution execution;
 
+    @Inject
+    private XWikiSyntaxEscaper escaper;
+
     /**
      * Display an icon with wiki code from the current {@link org.xwiki.icon.IconSet}.
      *
@@ -66,12 +71,7 @@ public class IconManagerScriptService implements ScriptService
      */
     public String render(String iconName)
     {
-        try {
-            return iconManager.render(iconName);
-        } catch (IconException e) {
-            setLastError(e);
-            return null;
-        }
+        return String.format("{{displayIcon name=\"%s\"/}}", escapeXWiki21(iconName));
     }
 
     /**
@@ -84,12 +84,8 @@ public String render(String iconName)
      */
     public String render(String iconName, String iconSetName)
     {
-        try {
-            return iconManager.render(iconName, iconSetName);
-        } catch (IconException e) {
-            setLastError(e);
-            return null;
-        }
+        return String.format("{{displayIcon name=\"%s\" iconSet=\"%s\"/}}", escapeXWiki21(iconName),
+            escapeXWiki21(iconSetName));
     }
 
     /**
@@ -103,12 +99,13 @@ public String render(String iconName, String iconSetName)
      */
     public String render(String iconName, String iconSetName, boolean fallback)
     {
-        try {
-            return iconManager.render(iconName, iconSetName, fallback);
-        } catch (IconException e) {
-            setLastError(e);
-            return null;
-        }
+        return String.format("{{displayIcon name=\"%s\" iconSet=\"%s\" fallback=\"%b\"/}}", escapeXWiki21(iconName),
+            escapeXWiki21(iconSetName), fallback);
+    }
+
+    private String escapeXWiki21(String value)
+    {
+        return this.escaper.escape(value, Syntax.XWIKI_2_1);
     }
 
     /**

programming_language: Java
filename: IconPicker.xml
change_type: ModificationType.MODIFY
diff: @@ -48,7 +48,7 @@
   #set($map = {})
   #set($discard = $map.put('iconThemes', $services.icon.iconSetNames))
   #set($discard = $map.put('currentIconTheme', $services.icon.currentIconSetName))
-  $jsontool.serialize($map)
+  #jsonResponse($map)
 ###########################
 ## DATA: ICONS
 ###########################
@@ -61,7 +61,7 @@
     #set($discard = $icon.put('render', $services.icon.renderHTML($xwikiIcon, $iconTheme)))
     #set($discard = $icons.add($icon))
   #end
-  $jsontool.serialize($icons)
+  #jsonResponse($icons)
 #else
 = Presentation =
 The Icon Picker is a jQuery plugin written by XWiki to help user selecting an icon. See [[IconPickerMacro]] for using this picker easily. If you want to use it manually, read the following.

programming_language: PHP
filename: IconPickerMacro.xml
change_type: ModificationType.MODIFY
diff: @@ -275,11 +275,11 @@
     <property>
       <code>{{velocity output="false"}}
   $xwiki.ssx.use('IconThemesCode.IconPicker')
-  ## The icons themes may need some SSX, so we ask for a rendering of an icon of each icon theme, to be able to display
-  ## all icon themes in the picker
+  ## The icons themes may need some SSX, so we ask to load the resources of all icon sets to be able to display them
+  ## in the picker.
   ## ToDo: since it is a bit hacky, a better system would be to dynamically load the needed SSX on demand
   #foreach($iconSetName in $services.icon.iconSetNames)
-    $services.icon.render('wiki', $iconSetName)
+    $services.icon.use($iconSetName)
   #end
 {{/velocity}}
 

programming_language: TypeScript
repo_url: https://github.com/xwiki/xwiki-platform
commit_details: repo_url: https://github.com/xwiki/xwiki-platform
author: Michael Hamann
author_date: 2023-03-02 13:46:28+01:00
msg: 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.
repository: repo_url: https://github.com/xwiki/xwiki-platform
repo_name: xwiki/xwiki-platform
description: The XWiki platform
repo_language: Java
owner: xwiki
file_changes: filename: pom.xml
change_type: ModificationType.MODIFY
diff: @@ -32,7 +32,7 @@
   <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>

programming_language: HTML
filename: DefaultDocumentContextExecutor.java
change_type: ModificationType.ADD
diff: @@ -0,0 +1,74 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.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);
+            }
+        }
+    }
+}

programming_language: Java
filename: DocumentContextExecutor.java
change_type: ModificationType.ADD
diff: @@ -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;
+}

programming_language: Java
filename: components.txt
change_type: ModificationType.ADD
diff: @@ -0,0 +1 @@
+org.xwiki.bridge.internal.DefaultDocumentContextExecutor

programming_language: SQL
filename: DefaultDocumentContextExecutorTest.java
change_type: ModificationType.ADD
diff: @@ -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());
+    }
+}

programming_language: Java
filename: IconException.java
change_type: ModificationType.MODIFY
diff: @@ -37,4 +37,16 @@ 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);
+    }
 }

programming_language: Java
filename: IconSet.java
change_type: ModificationType.MODIFY
diff: @@ -24,6 +24,9 @@
 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.
  *
@@ -52,6 +55,8 @@
 
     private IconType type;
 
+    private DocumentReference sourceDocumentReference;
+
     /**
      * Constructor.
      *
@@ -268,4 +273,27 @@ 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;
+    }
 }

programming_language: Java
filename: pom.xml
change_type: ModificationType.MODIFY
diff: @@ -59,6 +59,11 @@
       <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>

programming_language: HTML
filename: DefaultIconRenderer.java
change_type: ModificationType.MODIFY
diff: @@ -98,14 +98,14 @@ public String render(String iconName, IconSet iconSet, String renderer) throws I
         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);
@@ -120,19 +120,19 @@ 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());
     }
 }

programming_language: Java
filename: DefaultIconSetLoader.java
change_type: ModificationType.MODIFY
diff: @@ -25,6 +25,7 @@
 import java.util.Properties;
 
 import javax.inject.Inject;
+import javax.inject.Named;
 import javax.inject.Singleton;
 
 import org.xwiki.bridge.DocumentAccessBridge;
@@ -36,6 +37,9 @@
 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;
 
 /**
@@ -72,19 +76,38 @@ public class DefaultIconSetLoader implements IconSetLoader
     @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);
         }

programming_language: Java
filename: DefaultIconSetManager.java
change_type: ModificationType.MODIFY
diff: @@ -21,6 +21,7 @@
 
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.util.ArrayList;
 import java.util.List;
 
 import javax.inject.Inject;
@@ -29,6 +30,7 @@
 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;
@@ -48,6 +50,8 @@
 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}.
  *
@@ -89,6 +93,9 @@ public class DefaultIconSetManager implements IconSetManager
     @Named("all")
     private ConfigurationSource configurationSource;
 
+    @Inject
+    private Logger logger;
+
     @Override
     public IconSet getCurrentIconSet() throws IconException
     {
@@ -157,36 +164,69 @@ public IconSet getIconSet(String name) throws IconException
         }
 
         // 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;
     }
 

programming_language: Java
filename: VelocityRenderer.java
change_type: ModificationType.MODIFY
diff: @@ -20,18 +20,25 @@
 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.
@@ -43,19 +50,35 @@
 @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();
@@ -64,29 +87,51 @@ public String render(String code) throws IconException
         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
@@ -94,5 +139,12 @@ public String render(String code) throws IconException
                 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.");
+        }
     }
 }

programming_language: Java
filename: DefaultIconRendererTest.java
change_type: ModificationType.MODIFY
diff: @@ -75,7 +75,8 @@ 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);
@@ -94,7 +95,7 @@ public void renderWithCSS() throws Exception
         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);
@@ -148,7 +149,7 @@ public void renderHTML() throws Exception
         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
@@ -165,7 +166,7 @@ public void renderHTMLWithCSS() throws Exception
         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);
@@ -230,8 +231,8 @@ 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;
@@ -251,7 +252,7 @@ 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");

programming_language: Java
filename: DefaultIconSetLoaderTest.java
change_type: ModificationType.MODIFY
diff: @@ -32,16 +32,19 @@
 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;
 
@@ -52,7 +55,7 @@
  * @since 6.2M1
  */
 @ComponentTest
-public class DefaultIconSetLoaderTest
+class DefaultIconSetLoaderTest
 {
     @InjectMockComponents
     private DefaultIconSetLoader iconSetLoader;
@@ -64,9 +67,9 @@
     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
@@ -85,12 +88,12 @@ private void verifies(IconSet result) throws Exception
     }
 
     @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);
@@ -98,20 +101,22 @@ public void loadIconSet() throws Exception
     }
 
     @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);
@@ -119,43 +124,30 @@ public void loadIconSetFromWikiDocument() throws Exception
     }
 
     @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());
     }
 }

programming_language: Java
filename: DefaultIconSetManagerTest.java
change_type: ModificationType.MODIFY
diff: @@ -31,6 +31,7 @@
 
 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;
@@ -44,6 +45,8 @@
 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;
@@ -55,6 +58,7 @@
 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;
@@ -112,6 +116,9 @@
     @Mock
     private XWiki xwiki;
 
+    @RegisterExtension
+    private LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);
+
     @BeforeEach
     void setUp()
     {
@@ -326,6 +333,70 @@ void getIconSetWhenException() throws Exception
         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
     {

programming_language: Java
filename: VelocityRendererTest.java
change_type: ModificationType.MODIFY
diff: @@ -20,21 +20,33 @@
 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;
@@ -47,41 +59,44 @@
  * @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());
@@ -89,17 +104,17 @@ public Object answer(InvocationOnMock invocation) throws Throwable
     }
 
     @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;
         }
 
@@ -110,28 +125,54 @@ public void renderWithException() throws Exception
     }
 
     @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));
+    }
 }

programming_language: Java
filename: DefaultIconThemesResourceTest.java
change_type: ModificationType.MODIFY
diff: @@ -229,7 +229,7 @@ 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"));

programming_language: Java
filename: XWikiSyntaxEscaper.java
change_type: ModificationType.ADD
diff: @@ -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));
+    }
+}

programming_language: Java
filename: RenderingScriptService.java
change_type: ModificationType.MODIFY
diff: @@ -38,6 +38,7 @@
 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;
@@ -90,6 +91,9 @@ public class RenderingScriptService implements ScriptService
     @Inject
     private MacroIdFactory macroIdFactory;
 
+    @Inject
+    private XWikiSyntaxEscaper escaper;
+
     /**
      * @return the list of syntaxes for which a Parser is available
      */
@@ -215,29 +219,7 @@ public Syntax resolveSyntax(String syntaxId)
      */
     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);
     }
 
     /**
@@ -330,15 +312,4 @@ 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));
-    }
 }

programming_language: Java
filename: components.txt
change_type: ModificationType.MODIFY
diff: @@ -12,3 +12,4 @@ org.xwiki.rendering.internal.resolver.PageResourceReferenceEntityReferenceResolv
 org.xwiki.rendering.internal.resolver.SpaceResourceReferenceEntityReferenceResolver
 org.xwiki.rendering.script.RenderingScriptService
 org.xwiki.rendering.internal.parser.LinkParser
+org.xwiki.rendering.internal.util.XWikiSyntaxEscaper

programming_language: Scala
filename: RenderingScriptServiceTest.java
change_type: ModificationType.MODIFY
diff: @@ -31,6 +31,7 @@
 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;
@@ -74,7 +75,7 @@
  * @since 3.2M3
  */
 @ComponentTest
-@ComponentList({ ContextComponentManagerProvider.class })
+@ComponentList({ ContextComponentManagerProvider.class, XWikiSyntaxEscaper.class })
 class RenderingScriptServiceTest
 {
     @InjectMockComponents

programming_language: Java