Skip to content
Snippets Groups Projects
Commit ca189cbe authored by Marius Dumitru Florea's avatar Marius Dumitru Florea
Browse files

XWIKI-21949: Restrict the execution of script macros during a realtime WYSIWYG editing session

* Add more unit tests.

(cherry picked from commit d3c526ea)
parent ad3dbb98
No related branches found
No related tags found
No related merge requests found
Showing
with 549 additions and 10 deletions
......@@ -34,7 +34,7 @@
<properties>
<!-- Name to display by the Extension Manager -->
<xwiki.extension.name>Netflux API</xwiki.extension.name>
<xwiki.jacoco.instructionRatio>0.84</xwiki.jacoco.instructionRatio>
<xwiki.jacoco.instructionRatio>0.89</xwiki.jacoco.instructionRatio>
</properties>
<dependencies>
<dependency>
......
......@@ -86,7 +86,7 @@ public class EntityChannelScriptAuthorTracker
@Named("explicit")
private EntityReferenceResolver<String> explicitEntityReferenceResolver;
private Map<String, EntityChange> scriptAuthors = new ConcurrentHashMap<>();
private final Map<String, EntityChange> scriptAuthors = new ConcurrentHashMap<>();
/**
* @param channelId identifies the entity channel for which to return the script author
......
......@@ -54,4 +54,12 @@ void hashCodeEquals()
channels.add(bob);
assertEquals(2, channels.size());
}
@Test
void toStringTest()
{
EntityChannel alice =
new EntityChannel(new DocumentReference("foo", "Some", "Page"), List.of("en", "content", "wiki"), "123456");
assertEquals("entity = [foo:Some.Page], path = [[en], [content], [wiki]], key = [123456]", alice.toString());
}
}
......@@ -19,6 +19,11 @@
*/
package org.xwiki.netflux.internal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.Mockito.when;
import java.util.List;
import javax.websocket.Session;
......@@ -31,11 +36,6 @@
import org.xwiki.test.junit5.mockito.InjectMockComponents;
import org.xwiki.test.junit5.mockito.MockComponent;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.Mockito.when;
/**
* Unit tests for {@link DefaultEntityChannelStore}.
*
......@@ -82,6 +82,7 @@ void createAndGetChannel()
// Get should return the existing channel.
assertSame(entityChannel, this.entityChannelStore.getChannel(this.entityReference, path).get());
assertSame(entityChannel, this.entityChannelStore.getChannel(channel.getKey()).get());
assertEquals(1, entityChannel.getUserCount());
// Disconnect the user and check again the user count.
......@@ -91,6 +92,7 @@ void createAndGetChannel()
// Disconnect the raw channel and check the entity channel.
when(this.channelStore.get(channel.getKey())).thenReturn(null);
assertFalse(this.entityChannelStore.getChannel(this.entityReference, path).isPresent());
assertFalse(this.entityChannelStore.getChannel(channel.getKey()).isPresent());
}
@Test
......@@ -98,8 +100,7 @@ void getChannels()
{
Channel channelOne = new Channel("one");
when(this.channelStore.create()).thenReturn(channelOne);
EntityChannel entityChannelOne =
this.entityChannelStore.createChannel(this.entityReference, List.of("a", "b"));
EntityChannel entityChannelOne = this.entityChannelStore.createChannel(this.entityReference, List.of("a", "b"));
when(this.channelStore.get(channelOne.getKey())).thenReturn(channelOne);
Channel channelTwo = new Channel("two");
......
/*
* 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.netflux.internal;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Arrays;
import java.util.Locale;
import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.bridge.DocumentModelBridge;
import org.xwiki.bridge.event.ActionExecutingEvent;
import org.xwiki.container.Container;
import org.xwiki.container.Request;
import org.xwiki.model.reference.AttachmentReference;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.netflux.internal.EntityChange.ScriptLevel;
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;
/**
* Unit tests for {@link EffectiveAuthorSetterListener}.
*
* @version $Id$
*/
@ComponentTest
class EffectiveAuthorSetterListenerTest
{
@InjectMockComponents
private EffectiveAuthorSetterListener listener;
@MockComponent
private EntityChannelScriptAuthorTracker scriptAuthorTracker;
@MockComponent
private DocumentAccessBridge documentAccessBridge;
@MockComponent
private Container container;
@Mock
private Request request;
@Mock
private UserReference effectiveAuthor;
@BeforeEach
void beforeEach()
{
when(this.container.getRequest()).thenReturn(this.request);
}
@Test
void onActionExecutingEvent() throws Exception
{
DocumentReference currentDocumentReference = new DocumentReference("test", "Some", "Page");
when(this.documentAccessBridge.getCurrentDocumentReference()).thenReturn(currentDocumentReference);
DocumentModelBridge tdoc = mock(DocumentModelBridge.class);
when(this.documentAccessBridge.getTranslatedDocumentInstance(currentDocumentReference)).thenReturn(tdoc);
when(tdoc.getRealLanguage()).thenReturn("fr");
UserReference otherUserReference = mock(UserReference.class);
// An entity change that targets a different document.
EntityChange entityChangeTwo =
new EntityChange(new DocumentReference("test", "Other", "Page"), otherUserReference, ScriptLevel.SCRIPT);
// An entity change that targets the current document translation, but with a higher script level.
EntityChange entityChangeThree =
new EntityChange(new DocumentReference(currentDocumentReference, Locale.FRENCH), otherUserReference,
ScriptLevel.PROGRAMMING);
// An entity change that targets the current document (access rights are checked at document level) with a lower
// script level. The lower script level should win.
EntityChange entityChangeFour = new EntityChange(new AttachmentReference("file.txt", currentDocumentReference),
this.effectiveAuthor, ScriptLevel.SCRIPT);
when(this.request.getProperties("netfluxChannel"))
.thenReturn(Arrays.asList("", "one", null, "two", "three", "four"));
when(this.scriptAuthorTracker.getScriptAuthor("two")).thenReturn(Optional.of(entityChangeTwo));
when(this.scriptAuthorTracker.getScriptAuthor("three")).thenReturn(Optional.of(entityChangeThree));
when(this.scriptAuthorTracker.getScriptAuthor("four")).thenReturn(Optional.of(entityChangeFour));
this.listener.onEvent(new ActionExecutingEvent(), null, null);
verify(this.request).setProperty("com.xpn.xwiki.web.XWikiRequest#effectiveAuthor", this.effectiveAuthor);
}
}
/*
* 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.netflux.internal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.junit.jupiter.api.Test;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.netflux.internal.EntityChange.ScriptLevel;
import org.xwiki.user.UserReference;
/**
* Unit tests for {@link EntityChange}.
*
* @version $Id$
*/
class EntityChangeTest
{
private DocumentReference documentReference = new DocumentReference("test", "Some", "Page");
private UserReference userReference = mock(UserReference.class);
@Test
void verifyToString()
{
when(this.userReference.toString()).thenReturn("Alice");
EntityChange entityChange = new EntityChange(this.documentReference, this.userReference, ScriptLevel.SCRIPT);
assertTrue(entityChange.toString()
.startsWith("entity = [test:Some.Page], author = [Alice], scriptLevel = [SCRIPT], timestamp = ["));
}
@Test
void verifyEquals() throws Exception
{
EntityChange firstChange = new EntityChange(this.documentReference, this.userReference, ScriptLevel.SCRIPT);
assertEquals(firstChange, firstChange);
// Entity change records the timestamp when it is created, so we need to wait at least 1 millisecond before
// creating the second instance.
Thread.sleep(1);
EntityChange secondChange = new EntityChange(this.documentReference, this.userReference, ScriptLevel.SCRIPT);
assertNotEquals(firstChange, secondChange);
}
}
/*
* 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.netflux.internal;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.List;
import java.util.Optional;
import javax.websocket.Session;
import org.junit.jupiter.api.Test;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.netflux.EntityChannel;
import org.xwiki.netflux.EntityChannelStore;
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.CurrentUserReference;
import org.xwiki.user.UserReference;
import org.xwiki.user.UserReferenceResolver;
import org.xwiki.websocket.WebSocketContext;
/**
* Unit tests for {@link EntityChannelScriptAuthorBot}.
*
* @version $Id$
*/
@ComponentTest
class EntityChannelScriptAuthorBotTest
{
@InjectMockComponents
private EntityChannelScriptAuthorBot bot;
@MockComponent
private EntityChannelStore entityChannels;
@MockComponent
private UserReferenceResolver<CurrentUserReference> currentUserResolver;
@MockComponent
private WebSocketContext webSocketContext;
@MockComponent
private EntityChannelScriptAuthorTracker scriptAuthorTracker;
private DocumentReference documentReference = new DocumentReference("test", "Some", "Page");
private Channel channel = new Channel("one");
@Test
void onJoinChannel()
{
// No entity channel associated.
assertFalse(this.bot.onJoinChannel(this.channel));
// The associated entity channel doesn't need protection.
EntityChannel entityChannel =
new EntityChannel(this.documentReference, List.of("en", "content", "events"), this.channel.getKey());
when(this.entityChannels.getChannel(this.channel.getKey())).thenReturn(Optional.of(entityChannel));
assertFalse(this.bot.onJoinChannel(this.channel));
// The associated entity channel needs protection.
entityChannel =
new EntityChannel(this.documentReference, List.of("en", "content", "wiki"), this.channel.getKey());
when(this.entityChannels.getChannel(this.channel.getKey())).thenReturn(Optional.of(entityChannel));
assertTrue(this.bot.onJoinChannel(this.channel));
// The associated entity channel doesn't need protection.
entityChannel = new EntityChannel(this.documentReference, List.of(), this.channel.getKey());
when(this.entityChannels.getChannel(this.channel.getKey())).thenReturn(Optional.of(entityChannel));
assertFalse(this.bot.onJoinChannel(this.channel));
// The associated entity channel needs protection.
entityChannel =
new EntityChannel(this.documentReference, List.of("en", "property", "wysiwyg"), this.channel.getKey());
when(this.entityChannels.getChannel(this.channel.getKey())).thenReturn(Optional.of(entityChannel));
assertTrue(this.bot.onJoinChannel(this.channel));
}
@Test
void onChannelMessage()
{
User sender = mock(User.class);
Session session = mock(Session.class);
when(sender.getSession()).thenReturn(session);
doAnswer(invocation -> {
Runnable runnable = invocation.getArgument(1);
runnable.run();
return null;
}).when(this.webSocketContext).run(any(Session.class), any(Runnable.class));
// Not a command message.
this.bot.onChannelMessage(this.channel, sender, "PING", null);
verify(this.scriptAuthorTracker, never()).maybeUpdateScriptAuthor(any(EntityChannel.class),
any(UserReference.class));
// No entity channel associated.
this.bot.onChannelMessage(this.channel, sender, "MSG", null);
verify(this.scriptAuthorTracker, never()).maybeUpdateScriptAuthor(any(EntityChannel.class),
any(UserReference.class));
// The message is send to an entity channel.
UserReference currentUserReference = mock(UserReference.class);
when(this.currentUserResolver.resolve(CurrentUserReference.INSTANCE)).thenReturn(currentUserReference);
EntityChannel entityChannel =
new EntityChannel(this.documentReference, List.of("en", "content", "wiki"), this.channel.getKey());
when(this.entityChannels.getChannel(this.channel.getKey())).thenReturn(Optional.of(entityChannel));
this.bot.onChannelMessage(this.channel, sender, "MSG", null);
verify(this.scriptAuthorTracker).maybeUpdateScriptAuthor(entityChannel, currentUserReference);
}
}
/*
* 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.netflux.internal;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.mockito.Mockito.when;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import javax.inject.Named;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.xwiki.bridge.DocumentAccessBridge;
import org.xwiki.bridge.DocumentModelBridge;
import org.xwiki.model.EntityType;
import org.xwiki.model.document.DocumentAuthors;
import org.xwiki.model.reference.DocumentReference;
import org.xwiki.model.reference.EntityReferenceResolver;
import org.xwiki.model.reference.ObjectPropertyReference;
import org.xwiki.model.reference.ObjectReference;
import org.xwiki.netflux.EntityChannel;
import org.xwiki.netflux.EntityChannelStore;
import org.xwiki.security.authorization.AuthorizationManager;
import org.xwiki.security.authorization.Right;
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.CurrentUserReference;
import org.xwiki.user.UserReference;
import org.xwiki.user.UserReferenceResolver;
import org.xwiki.user.UserReferenceSerializer;
/**
* Unit tests for {@link EntityChannelScriptAuthorTracker}.
*
* @version $Id$
*/
@ComponentTest
class EntityChannelScriptAuthorTrackerTest
{
@InjectMockComponents
private EntityChannelScriptAuthorTracker tracker;
@MockComponent
private EntityChannelStore entityChannels;
@MockComponent
private AuthorizationManager authorizationManager;
@MockComponent
private DocumentAccessBridge documentAccessBridge;
@MockComponent
@Named("document")
private UserReferenceSerializer<DocumentReference> documentUserReferenceSerializer;
@MockComponent
private UserReferenceResolver<CurrentUserReference> currentUserResolver;
@MockComponent
@Named("explicit")
private EntityReferenceResolver<String> explicitEntityReferenceResolver;
@Mock(name = "alice")
private UserReference alice;
@Mock(name = "bob")
private UserReference bob;
@Mock(name = "carol")
private UserReference carol;
private DocumentReference documentReference = new DocumentReference("test", "Space", "Page");
private DocumentReference translationReference = new DocumentReference(this.documentReference, Locale.FRENCH);
private ObjectPropertyReference objectPropertyReference =
new ObjectPropertyReference("text", new ObjectReference("Some.Class[0]", this.documentReference));
@Mock
private DocumentModelBridge document;
@Mock
DocumentAuthors documentAuthors;
@BeforeEach
void beforeEach()
{
when(this.document.getAuthors()).thenReturn(this.documentAuthors);
when(this.documentAuthors.getContentAuthor()).thenReturn(this.carol);
when(this.documentAuthors.getEffectiveMetadataAuthor()).thenReturn(this.carol);
// Alice has only script rights.
DocumentReference aliceReference = new DocumentReference("xwiki", "Users", "Alice");
when(this.documentUserReferenceSerializer.serialize(this.alice)).thenReturn(aliceReference);
when(this.authorizationManager.hasAccess(Right.SCRIPT, aliceReference, this.documentReference))
.thenReturn(true);
when(this.authorizationManager.hasAccess(Right.SCRIPT, aliceReference, this.translationReference))
.thenReturn(true);
when(this.authorizationManager.hasAccess(Right.SCRIPT, aliceReference, this.objectPropertyReference))
.thenReturn(true);
// Bob has no script rights.
DocumentReference bobReference = new DocumentReference("xwiki", "Users", "Bob");
when(this.documentUserReferenceSerializer.serialize(this.bob)).thenReturn(bobReference);
// Carol has programming rights.
DocumentReference carolReference = new DocumentReference("xwiki", "Users", "Carol");
when(this.documentUserReferenceSerializer.serialize(this.carol)).thenReturn(carolReference);
when(this.authorizationManager.hasAccess(Right.PROGRAM, carolReference, this.documentReference))
.thenReturn(true);
when(this.authorizationManager.hasAccess(Right.PROGRAM, carolReference, this.translationReference))
.thenReturn(true);
when(this.authorizationManager.hasAccess(Right.PROGRAM, carolReference, this.objectPropertyReference))
.thenReturn(true);
when(this.currentUserResolver.resolve(CurrentUserReference.INSTANCE)).thenReturn(this.carol);
when(this.explicitEntityReferenceResolver.resolve("Some.Class[0].text", EntityType.OBJECT_PROPERTY,
documentReference)).thenReturn(this.objectPropertyReference);
}
@Test
void getScriptAuthorForDocumentContent() throws Exception
{
// No messages received yet on this channel.
assertFalse(this.tracker.getScriptAuthor("channelKey").isPresent());
EntityChannel entityChannel =
new EntityChannel(documentReference, List.of("fr", "content", "wiki"), "channelKey");
when(this.entityChannels.getChannel(entityChannel.getKey())).thenReturn(Optional.of(entityChannel));
when(this.documentAccessBridge.getTranslatedDocumentInstance(this.translationReference))
.thenReturn(this.document);
// Alice sends a message on the channel.
this.tracker.maybeUpdateScriptAuthor(entityChannel, alice);
assertEquals(alice, this.tracker.getScriptAuthor("channelKey").get().getAuthor());
// Bob sends a message on the channel. Bob has less script rights than Alice.
this.tracker.maybeUpdateScriptAuthor(entityChannel, bob);
assertEquals(bob, this.tracker.getScriptAuthor("channelKey").get().getAuthor());
// Alice sends another message on the channel, but it should not change the script author because Alice has more
// script rights than Bob (we can only lower the script level).
this.tracker.maybeUpdateScriptAuthor(entityChannel, alice);
// Close the entity channel.
when(this.entityChannels.getChannel(entityChannel.getKey())).thenReturn(Optional.empty());
// We can still get the script author once.
assertEquals(bob, this.tracker.getScriptAuthor("channelKey").get().getAuthor());
// The script author was reset.
assertFalse(this.tracker.getScriptAuthor("channelKey").isPresent());
}
@Test
void getScriptAuthorForDocumentMetaData() throws Exception
{
// No messages received yet on this channel.
assertFalse(this.tracker.getScriptAuthor("channelKey").isPresent());
EntityChannel entityChannel =
new EntityChannel(documentReference, List.of("en", "Some.Class[0].text", "wysiwyg"), "channelKey");
when(this.entityChannels.getChannel(entityChannel.getKey())).thenReturn(Optional.of(entityChannel));
when(this.documentAccessBridge.getDocumentInstance(this.documentReference)).thenReturn(this.document);
// Alice sends a message on the channel.
this.tracker.maybeUpdateScriptAuthor(entityChannel, alice);
assertEquals(alice, this.tracker.getScriptAuthor("channelKey").get().getAuthor());
// Bob sends a message on the channel. Bob has less script rights than Alice.
this.tracker.maybeUpdateScriptAuthor(entityChannel, bob);
assertEquals(bob, this.tracker.getScriptAuthor("channelKey").get().getAuthor());
// Alice sends another message on the channel, but it should not change the script author because Alice has more
// script rights than Bob (we can only lower the script level).
this.tracker.maybeUpdateScriptAuthor(entityChannel, alice);
// Close the entity channel.
when(this.entityChannels.getChannel(entityChannel.getKey())).thenReturn(Optional.empty());
// We can still get the script author once.
assertEquals(bob, this.tracker.getScriptAuthor("channelKey").get().getAuthor());
// The script author was reset.
assertFalse(this.tracker.getScriptAuthor("channelKey").isPresent());
}
}
......@@ -161,7 +161,7 @@ com.xpn.xwiki.internal.render.groovy.ParseGroovyFromString
com.xpn.xwiki.internal.user.MyPersistentLoginManagerProvider
com.xpn.xwiki.internal.user.UserAuthenticatedEventNotifier
com.xpn.xwiki.internal.velocity.DefaultVelocityEvaluator
com.xpn.xwiki.internal.web.EffectiveAuthorSetterListener
org.xwiki.internal.web.EffectiveAuthorSetterListener
org.xwiki.internal.web.GetDocExistValidator
org.xwiki.internal.web.ViewDocExistValidator
com.xpn.xwiki.objects.meta.BooleanMetaClass
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment