Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

import javax.inject.Inject;
Expand All @@ -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;
Expand All @@ -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}.
*
Expand Down Expand Up @@ -89,6 +93,9 @@ public class DefaultIconSetManager implements IconSetManager
@Named("all")
private ConfigurationSource configurationSource;

@Inject
private Logger logger;

@Override
public IconSet getCurrentIconSet() throws IconException
{
Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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();
Expand All @@ -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.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -52,7 +55,7 @@
* @since 6.2M1
*/
@ComponentTest
public class DefaultIconSetLoaderTest
class DefaultIconSetLoaderTest
{
@InjectMockComponents
private DefaultIconSetLoader iconSetLoader;
Expand All @@ -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
Expand All @@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -112,6 +116,9 @@ class DefaultIconSetManagerTest
@Mock
private XWiki xwiki;

@RegisterExtension
private LogCaptureExtension logCapture = new LogCaptureExtension(LogLevel.WARN);

@BeforeEach
void setUp()
{
Expand Down Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

Expand All @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -74,7 +75,7 @@
* @since 3.2M3
*/
@ComponentTest
@ComponentList({ ContextComponentManagerProvider.class })
@ComponentList({ ContextComponentManagerProvider.class, XWikiSyntaxEscaper.class })
class RenderingScriptServiceTest
{
@InjectMockComponents
Expand Down