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

Severity: nan (CVSS v3 base score: 8.8)
Weakness: CWE-74 \u2014 Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
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.
Repository: https://github.com/xwiki/xwiki-platform
Files changed:
xwiki-platform-core/xwiki-platform-icon/pom.xml
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/pom.xml
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/src/main/java/org/xwiki/icon/macro/DisplayIconMacroParameters.java
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/src/main/java/org/xwiki/icon/macro/internal/DisplayIconMacro.java
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/src/main/resources/ApplicationResources.properties
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/src/main/resources/META-INF/components.txt
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/src/test/java/org/xwiki/icon/macro/IntegrationTests.java
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/src/test/java/org/xwiki/icon/macro/internal/DisplayIconMacroTest.java
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/src/test/resources/macrodisplayicon1.test
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/src/test/resources/macrodisplayicon2.test
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/src/test/resources/macrodisplayicon3.test
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/src/test/resources/macrodisplayicon4.test
Commit message: 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.


Vulnerable code:

Fixed code:
    <module>xwiki-platform-icon-macro</module>
<?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>
/*
 * 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;
    }
}
/*
 * 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;
    }
}
# ---------------------------------------------------------------------------
# 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.

org.xwiki.icon.macro.internal.DisplayIconMacro
/*
 * 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);
    }
}
/*
 * 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));
    }
}
.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
.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
.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
.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