Skip to content

Commit 98b4629

Browse files
michituxgithub-actions[bot]
authored andcommitted
XRENDERING-771: Null pointer exception when list items occur without lists (#327)
* Protect against list items without wrapping list. * Log a debug message to make it possible to debug this problem. * Add tests. (cherry picked from commit 5f70b11)
1 parent 04155ee commit 98b4629

File tree

2 files changed

+91
-4
lines changed

2 files changed

+91
-4
lines changed

xwiki-rendering-api/src/main/java/org/xwiki/rendering/listener/chaining/BlockStateChainingListener.java

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import java.util.Deque;
2424
import java.util.Map;
2525

26+
import org.slf4j.Logger;
27+
import org.slf4j.LoggerFactory;
2628
import org.xwiki.rendering.listener.Format;
2729
import org.xwiki.rendering.listener.HeaderLevel;
2830
import org.xwiki.rendering.listener.ListType;
@@ -38,6 +40,8 @@
3840
*/
3941
public class BlockStateChainingListener extends AbstractChainingListener implements StackableChainingListener
4042
{
43+
private static final Logger LOGGER = LoggerFactory.getLogger(BlockStateChainingListener.class);
44+
4145
public enum Event
4246
{
4347
NONE,
@@ -278,7 +282,12 @@ public void beginDocument(MetaData metadata)
278282
public void beginDefinitionDescription()
279283
{
280284
++this.inlineDepth;
281-
++this.definitionListDepth.peek().definitionListItemIndex;
285+
if (!this.definitionListDepth.isEmpty()) {
286+
++this.definitionListDepth.peek().definitionListItemIndex;
287+
} else if (LOGGER.isDebugEnabled()) {
288+
LOGGER.debug("Invalid nesting: definition description outside definition list.",
289+
new IllegalStateException());
290+
}
282291

283292
super.beginDefinitionDescription();
284293

@@ -304,7 +313,11 @@ public void beginDefinitionList(Map<String, String> parameters)
304313
public void beginDefinitionTerm()
305314
{
306315
++this.inlineDepth;
307-
++this.definitionListDepth.peek().definitionListItemIndex;
316+
if (!this.definitionListDepth.isEmpty()) {
317+
++this.definitionListDepth.peek().definitionListItemIndex;
318+
} else if (LOGGER.isDebugEnabled()) {
319+
LOGGER.debug("Invalid nesting: definition term outside definition list.", new IllegalStateException());
320+
}
308321

309322
super.beginDefinitionTerm();
310323

@@ -340,7 +353,11 @@ public void beginList(ListType type, Map<String, String> parameters)
340353
public void beginListItem()
341354
{
342355
++this.inlineDepth;
343-
++this.listDepth.peek().listItemIndex;
356+
if (!this.listDepth.isEmpty()) {
357+
++this.listDepth.peek().listItemIndex;
358+
} else if (LOGGER.isDebugEnabled()) {
359+
LOGGER.debug("Invalid nesting: list item outside list.", new IllegalStateException());
360+
}
344361

345362
super.beginListItem();
346363

@@ -351,7 +368,11 @@ public void beginListItem()
351368
public void beginListItem(Map<String, String> parameters)
352369
{
353370
++this.inlineDepth;
354-
++this.listDepth.peek().listItemIndex;
371+
if (!this.listDepth.isEmpty()) {
372+
++this.listDepth.peek().listItemIndex;
373+
} else if (LOGGER.isDebugEnabled()) {
374+
LOGGER.debug("Invalid nesting: list item with parameters outside list.", new IllegalStateException());
375+
}
355376

356377
super.beginListItem(parameters);
357378

xwiki-rendering-api/src/test/java/org/xwiki/rendering/listener/chaining/BlockStateChainingListenerTest.java

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,23 @@
2121

2222
import java.lang.reflect.InvocationTargetException;
2323
import java.lang.reflect.Method;
24+
import java.util.Map;
2425

2526
import org.apache.commons.text.CaseUtils;
2627
import org.junit.jupiter.api.BeforeEach;
28+
import org.junit.jupiter.api.extension.RegisterExtension;
2729
import org.junit.jupiter.params.ParameterizedTest;
2830
import org.junit.jupiter.params.provider.MethodSource;
31+
import org.junit.jupiter.params.provider.ValueSource;
2932
import org.mockito.stubbing.Stubber;
3033
import org.xwiki.rendering.listener.ListType;
3134
import org.xwiki.rendering.listener.Listener;
3235
import org.xwiki.rendering.listener.MetaData;
36+
import org.xwiki.test.LogLevel;
37+
import org.xwiki.test.junit5.LogCaptureExtension;
3338

3439
import static org.junit.jupiter.api.Assertions.assertEquals;
40+
import static org.junit.jupiter.api.Assertions.assertFalse;
3541
import static org.junit.jupiter.api.Assertions.assertNotNull;
3642
import static org.junit.jupiter.api.Assertions.assertNull;
3743
import static org.mockito.Mockito.doAnswer;
@@ -50,6 +56,9 @@ class BlockStateChainingListenerTest
5056

5157
private ChainingListener mockListener;
5258

59+
@RegisterExtension
60+
private LogCaptureExtension logCaptureExtension = new LogCaptureExtension(LogLevel.DEBUG);
61+
5362
@BeforeEach
5463
void setUpChain()
5564
{
@@ -188,4 +197,61 @@ void testOnMethod(Method method, Object[] parameters) throws InvocationTargetExc
188197

189198
this.listener.endDocument(MetaData.EMPTY);
190199
}
200+
201+
@ParameterizedTest
202+
@ValueSource(booleans = { true, false })
203+
void testListItemWithoutParent(boolean withParameter) throws Exception
204+
{
205+
Class<?>[] parameterTypes = withParameter ? new Class<?>[] { Map.class } : new Class<?>[0];
206+
Method beginMethod = BlockStateChainingListener.class.getDeclaredMethod("beginListItem", parameterTypes);
207+
Method endMethod = BlockStateChainingListener.class.getDeclaredMethod("endListItem", parameterTypes);
208+
Object[] parameters = withParameter ? new Object[] { Listener.EMPTY_PARAMETERS } : new Object[0];
209+
210+
this.listener.beginDocument(MetaData.EMPTY);
211+
beginMethod.invoke(this.listener, parameters);
212+
assertEquals(-1, this.listener.getListItemIndex());
213+
assertFalse(this.listener.isInList());
214+
assertEquals(0, this.listener.getListDepth());
215+
endMethod.invoke(this.listener, parameters);
216+
beginMethod.invoke(this.listener, parameters);
217+
assertEquals(-1, this.listener.getListItemIndex());
218+
assertFalse(this.listener.isInList());
219+
assertEquals(0, this.listener.getListDepth());
220+
endMethod.invoke(this.listener, parameters);
221+
this.listener.endDocument(MetaData.EMPTY);
222+
223+
assertEquals(2, this.logCaptureExtension.size());
224+
String expectedMessage =
225+
"Invalid nesting: list item" + (withParameter ? " with parameters" : "") + " outside list.";
226+
for (int i = 0; i < 2; ++i) {
227+
assertEquals(expectedMessage, this.logCaptureExtension.getLogEvent(i).getMessage());
228+
}
229+
}
230+
231+
@ParameterizedTest
232+
@ValueSource(strings = { "Term", "Description" })
233+
void testDefinitionListItemWithoutParent(String methodSuffix) throws Exception
234+
{
235+
Method beginMethod = BlockStateChainingListener.class.getDeclaredMethod("beginDefinition" + methodSuffix);
236+
Method endMethod = BlockStateChainingListener.class.getDeclaredMethod("endDefinition" + methodSuffix);
237+
this.listener.beginDocument(MetaData.EMPTY);
238+
beginMethod.invoke(this.listener);
239+
assertFalse(this.listener.isInDefinitionList());
240+
assertEquals(-1, this.listener.getDefinitionListItemIndex());
241+
assertEquals(0, this.listener.getDefinitionListDepth());
242+
endMethod.invoke(this.listener);
243+
beginMethod.invoke(this.listener);
244+
assertFalse(this.listener.isInDefinitionList());
245+
assertEquals(-1, this.listener.getDefinitionListItemIndex());
246+
assertEquals(0, this.listener.getDefinitionListDepth());
247+
endMethod.invoke(this.listener);
248+
this.listener.endDocument(MetaData.EMPTY);
249+
250+
assertEquals(2, this.logCaptureExtension.size());
251+
String expectedMessage =
252+
"Invalid nesting: definition " + methodSuffix.toLowerCase() + " outside definition list.";
253+
for (int i = 0; i < 2; ++i) {
254+
assertEquals(expectedMessage, this.logCaptureExtension.getLogEvent(i).getMessage());
255+
}
256+
}
191257
}

0 commit comments

Comments
 (0)