Code injection in icon themes of XWiki Platform
Description
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 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.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
org.xwiki.platform:xwiki-platform-icon-defaultMaven | >= 6.2-milestone-1, < 14.10.6 | 14.10.6 |
org.xwiki.platform:xwiki-platform-icon-scriptMaven | >= 6.2-milestone-1, < 14.10.6 | 14.10.6 |
org.xwiki.platform:xwiki-platform-icon-scriptMaven | >= 15.0-rc-1, < 15.2-rc-1 | 15.2-rc-1 |
org.xwiki.platform:xwiki-platform-icon-defaultMaven | >= 15.0-rc-1, < 15.2-rc-1 | 15.2-rc-1 |
org.xwiki.platform:xwiki-platform-icon-uiMaven | >= 6.2-milestone-1, < 14.10.6 | 14.10.6 |
org.xwiki.platform:xwiki-platform-icon-uiMaven | >= 15.0-rc-1, < 15.2-rc-1 | 15.2-rc-1 |
Affected products
1- Range: >= 6.2-milestone-1, < 14.10.6
Patches
379418dd92ca1XWIKI-20524: Improve icon rendering
4 files changed · +31 −23
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-script/pom.xml+11 −0 modified@@ -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
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-script/src/main/java/org/xwiki/icon/IconManagerScriptService.java+15 −18 modified@@ -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); } /**
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-ui/src/main/resources/IconThemesCode/IconPickerMacro.xml+3 −3 modified@@ -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}}
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-ui/src/main/resources/IconThemesCode/IconPicker.xml+2 −2 modified@@ -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.
46b542854978XWIKI-20682: Introduce an icon macro
12 files changed · +878 −0
xwiki-platform-core/xwiki-platform-icon/pom.xml+1 −0 modified@@ -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>
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/pom.xml+114 −0 added@@ -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
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/src/main/java/org/xwiki/icon/macro/DisplayIconMacroParameters.java+103 −0 added@@ -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; + } +}
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/src/main/java/org/xwiki/icon/macro/internal/DisplayIconMacro.java+193 −0 added@@ -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; + } +}
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/src/main/resources/ApplicationResources.properties+29 −0 added@@ -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. +
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/src/main/resources/META-INF/components.txt+1 −0 added@@ -0,0 +1 @@ +org.xwiki.icon.macro.internal.DisplayIconMacro
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/src/test/java/org/xwiki/icon/macro/IntegrationTests.java+124 −0 added@@ -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); + } +}
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/src/test/java/org/xwiki/icon/macro/internal/DisplayIconMacroTest.java+200 −0 added@@ -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)); + } +}
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/src/test/resources/macrodisplayicon1.test+36 −0 added@@ -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
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/src/test/resources/macrodisplayicon2.test+20 −0 added@@ -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
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/src/test/resources/macrodisplayicon3.test+37 −0 added@@ -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
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-macro/src/test/resources/macrodisplayicon4.test+20 −0 added@@ -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
b0cdfd893912XWIKI-20524: Improve icon rendering
21 files changed · +758 −162
xwiki-platform-core/xwiki-platform-bridge/pom.xml+1 −1 modified@@ -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>
xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/internal/DefaultDocumentContextExecutor.java+74 −0 added@@ -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); + } + } + } +}
xwiki-platform-core/xwiki-platform-bridge/src/main/java/org/xwiki/bridge/internal/DocumentContextExecutor.java+46 −0 added@@ -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; +}
xwiki-platform-core/xwiki-platform-bridge/src/main/resources/META-INF/components.txt+1 −0 added@@ -0,0 +1 @@ +org.xwiki.bridge.internal.DefaultDocumentContextExecutor
xwiki-platform-core/xwiki-platform-bridge/src/test/java/org/xwiki/bridge/internal/DefaultDocumentContextExecutorTest.java+147 −0 added@@ -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()); + } +}
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-api/src/main/java/org/xwiki/icon/IconException.java+12 −0 modified@@ -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); + } }
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-api/src/main/java/org/xwiki/icon/IconSet.java+28 −0 modified@@ -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 @@ public class IconSet 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; + } }
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/pom.xml+5 −0 modified@@ -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>
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/DefaultIconRenderer.java+7 −7 modified@@ -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()); } }
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/DefaultIconSetLoader.java+27 −4 modified@@ -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); }
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/DefaultIconSetManager.java+55 −15 modified@@ -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; }
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/main/java/org/xwiki/icon/internal/VelocityRenderer.java+62 −10 modified@@ -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,35 +87,64 @@ 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 if (engine != null) { engine.stoppedUsingMacroNamespace(namespace); } } + + if (result) { + return output.toString(); + } else { + // I don't know how to check the velocity runtime log + throw new IconException("Failed to render the icon. See the Velocity runtime log."); + } } }
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/DefaultIconRendererTest.java+8 −7 modified@@ -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");
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/DefaultIconSetLoaderTest.java+28 −36 modified@@ -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 @@ public class DefaultIconSetLoaderTest 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,77 +88,66 @@ 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); assertEquals("FontAwesome", result.getName()); } @Test - public void loadIconSetFromWikiDocument() throws Exception + void loadIconSetFromWikiDocument() throws Exception { DocumentReference iconSetRef = new DocumentReference("xwiki", "IconThemes", "Default"); DocumentReference iconClassRef = new DocumentReference("wikiId", "IconThemesCode", "IconThemeClass"); - when(documentAccessBridge.getProperty(eq(iconSetRef), eq(iconClassRef), eq("name"))).thenReturn("MyIconTheme"); + when(this.documentAccessBridge.getProperty(iconSetRef, iconClassRef, "name")).thenReturn("MyIconTheme"); DocumentModelBridge doc = mock(DocumentModelBridge.class); - when(documentAccessBridge.getDocumentInstance(iconSetRef)).thenReturn(doc); + when(this.documentAccessBridge.getDocumentInstance(iconSetRef)).thenReturn(doc); StringWriter content = new StringWriter(); IOUtils.copyLarge(new InputStreamReader(getClass().getResourceAsStream("/test.iconset")), content); when(doc.getContent()).thenReturn(content.toString()); + when(doc.getAuthors()).thenReturn(new DefaultDocumentAuthors(new XWikiDocument(iconSetRef))); + // Test - IconSet result = iconSetLoader.loadIconSet(iconSetRef); + IconSet result = this.iconSetLoader.loadIconSet(iconSetRef); // Verify verifies(result); assertEquals("MyIconTheme", result.getName()); } @Test - public void loadIconSetWithException() throws Exception + void loadIconSetWithException() throws Exception { Reader content = mock(Reader.class); IOException exception = new IOException("test"); when(content.read(any(char[].class))).thenThrow(exception); // Test - Exception caughException = null; - try { - iconSetLoader.loadIconSet(content, "FontAwesome"); - } catch (IconException e) { - caughException = e; - } - - assertNotNull(caughException); - assert (caughException instanceof IconException); - assertEquals(exception, caughException.getCause()); - assertEquals("Failed to load the IconSet [FontAwesome].", caughException.getMessage()); + Exception caughtException = assertThrows(IconException.class, () -> + this.iconSetLoader.loadIconSet(content, "FontAwesome")); + + assertEquals(exception, caughtException.getCause()); + assertEquals("Failed to load the IconSet [FontAwesome].", caughtException.getMessage()); } @Test - public void loadIconSetFromWikiDocumentWithException() throws Exception + void loadIconSetFromWikiDocumentWithException() throws Exception { Exception exception = new Exception("test"); - when(documentAccessBridge.getDocumentInstance(any(DocumentReference.class))).thenThrow(exception); + when(this.documentAccessBridge.getDocumentInstance(any(DocumentReference.class))).thenThrow(exception); - // Test - Exception caughException = null; - try { - iconSetLoader.loadIconSet(new DocumentReference("a", "b", "c")); - } catch (IconException e) { - caughException = e; - } - - assertNotNull(caughException); - assert (caughException instanceof IconException); - assertEquals(exception, caughException.getCause()); - assertEquals("Failed to load the IconSet [a:b.c].", caughException.getMessage()); + IconException caughtException = assertThrows(IconException.class, () -> + this.iconSetLoader.loadIconSet(new DocumentReference("a", "b", "c"))); + + assertEquals(exception, caughtException.getCause()); + assertEquals("Failed to load the IconSet [a:b.c].", caughtException.getMessage()); } }
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/DefaultIconSetManagerTest.java+71 −0 modified@@ -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 @@ class DefaultIconSetManagerTest @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 {
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-default/src/test/java/org/xwiki/icon/internal/VelocityRendererTest.java+87 −46 modified@@ -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,59 +59,62 @@ * @since 6.4M1 * @version $Id$ */ -public class VelocityRendererTest +@ComponentTest +class VelocityRendererTest { - @Rule - public MockitoComponentMockingRule<VelocityRenderer> mocker = - new MockitoComponentMockingRule<>(VelocityRenderer.class); - + @MockComponent private VelocityManager velocityManager; - @Before - public void setUp() throws Exception - { - velocityManager = mocker.getInstance(VelocityManager.class); - } + @MockComponent + private DocumentAccessBridge documentAccessBridge; + + @MockComponent + @Named("document") + private UserReferenceSerializer<DocumentReference> documentUserSerializer; + + @MockComponent + private AuthorExecutor authorExecutor; + + @MockComponent + private DocumentContextExecutor documentContextExecutor; + + @InjectMockComponents + private VelocityRenderer velocityRenderer; @Test - public void renderTest() throws Exception + void renderTest() throws Exception { // Mocks VelocityEngine engine = mock(VelocityEngine.class); - when(velocityManager.getVelocityEngine()).thenReturn(engine); + when(this.velocityManager.getVelocityEngine()).thenReturn(engine); when(engine.evaluate(any(VelocityContext.class), any(Writer.class), any(), eq("myCode"))).thenAnswer( - new Answer<Object>() - { - @Override - public Object answer(InvocationOnMock invocation) throws Throwable - { - // Get the writer - Writer writer = (Writer) invocation.getArguments()[1]; - writer.write("Rendered code"); - return true; - } - }); + invocation -> { + // Get the writer + Writer writer = (Writer) invocation.getArguments()[1]; + writer.write("Rendered code"); + return true; + }); // Test - assertEquals("Rendered code", mocker.getComponentUnderTest().render("myCode")); + assertEquals("Rendered code", this.velocityRenderer.render("myCode", null)); // Verify verify(engine).startedUsingMacroNamespace("IconVelocityRenderer_" + Thread.currentThread().getId()); verify(engine).stoppedUsingMacroNamespace("IconVelocityRenderer_" + Thread.currentThread().getId()); } @Test - public void renderWithException() throws Exception + void renderWithException() throws Exception { // Mocks Exception exception = new XWikiVelocityException("exception"); - when(velocityManager.getVelocityEngine()).thenThrow(exception); + when(this.velocityManager.getVelocityEngine()).thenThrow(exception); // Test IconException caughtException = null; try { - mocker.getComponentUnderTest().render("myCode"); - } catch(IconException e) { + this.velocityRenderer.render("myCode", null); + } catch (IconException e) { caughtException = e; } @@ -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)); + } }
xwiki-platform-core/xwiki-platform-icon/xwiki-platform-icon-rest/xwiki-platform-icon-rest-default/src/test/java/org/xwiki/icon/internal/DefaultIconThemesResourceTest.java+1 −1 modified@@ -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"));
xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/main/java/org/xwiki/rendering/internal/util/XWikiSyntaxEscaper.java+90 −0 added@@ -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)); + } +}
xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/main/java/org/xwiki/rendering/script/RenderingScriptService.java+5 −34 modified@@ -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)); - } }
xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/main/resources/META-INF/components.txt+1 −0 modified@@ -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
xwiki-platform-core/xwiki-platform-rendering/xwiki-platform-rendering-xwiki/src/test/java/org/xwiki/rendering/script/RenderingScriptServiceTest.java+2 −1 modified@@ -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
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-fm68-j7ww-h9xfghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-36470ghsaADVISORY
- github.com/xwiki/xwiki-platform/commit/46b542854978e9caa687a5c2b8817b8b17877d94ghsax_refsource_MISCWEB
- github.com/xwiki/xwiki-platform/commit/79418dd92ca11941b46987ef881bf50424898ff4ghsax_refsource_MISCWEB
- github.com/xwiki/xwiki-platform/commit/b0cdfd893912baaa053d106a92e39fa1858843c7ghsax_refsource_MISCWEB
- github.com/xwiki/xwiki-platform/security/advisories/GHSA-fm68-j7ww-h9xfghsax_refsource_CONFIRMWEB
- jira.xwiki.org/browse/XWIKI-20524ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.