You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@wicket.apache.org by pa...@apache.org on 2026/08/21 15:15:54 UTC

(wicket) 08/08: Escape the value an editable label shows for an empty model

This is an automated email from the ASF dual-hosted git repository.

papegaaij pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/wicket.git

commit 83dbc726eafe699ecc5fd78a5f1755de9503582c
Author: Emond Papegaaij <pa...@apache.org>
AuthorDate: Thu Aug 20 12:32:22 2026 +0200

    Escape the value an editable label shows for an empty model
    
    The label of each of the three editable labels writes defaultNullLabel() to the
    markup as it came when the model is empty, and hands the value over to the label
    component when it is not, which escapes it according to escapeModelStrings. So the
    placeholder was written as markup while the value beside it was escaped, and
    defaultNullLabel() is protected, so what it returns is not necessarily the
    constant the default implementation gives back.
    
    AjaxEditableChoiceLabel shows it plainest. One branch of its onComponentTagBody
    writes the value the IChoiceRenderer produced, which is escaped, and the other
    writes defaultNullLabel(), which was not.
    
    The value is escaped where it is written, following escapeModelStrings. The label
    takes that setting from the panel in onConfigure, next to the delegation that is
    already there, so turning escaping off on the panel turns it off for this value as
    well; the test covers both.
    
    Co-Authored-By: Claude Opus 5 (1M context) <no...@anthropic.com>
---
 .../ajax/markup/html/AjaxEditableChoiceLabel.java  |   4 +-
 .../ajax/markup/html/AjaxEditableLabel.java        |   5 +-
 .../markup/html/AjaxEditableMultiLineLabel.java    |   5 +-
 .../html/AjaxEditableLabelNullLabelEscapeTest.java | 153 +++++++++++++++++++++
 4 files changed, 164 insertions(+), 3 deletions(-)

diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/AjaxEditableChoiceLabel.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/AjaxEditableChoiceLabel.java
index 8ac6d85d90..d9cf78ed9e 100644
--- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/AjaxEditableChoiceLabel.java
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/AjaxEditableChoiceLabel.java
@@ -304,7 +304,9 @@ public class AjaxEditableChoiceLabel<T> extends AjaxEditableLabel<T>
 
 				if (Strings.isEmpty(displayValue))
 				{
-					replaceComponentTagBody(markupStream, openTag, defaultNullLabel());
+					String nullLabel = defaultNullLabel();
+					replaceComponentTagBody(markupStream, openTag,
+						getEscapeModelStrings() ? Strings.escapeMarkup(nullLabel) : nullLabel);
 				}
 				else
 				{
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/AjaxEditableLabel.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/AjaxEditableLabel.java
index d061ac4413..17368aa4b3 100644
--- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/AjaxEditableLabel.java
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/AjaxEditableLabel.java
@@ -40,6 +40,7 @@ import org.apache.wicket.model.IModel;
 import org.apache.wicket.model.IObjectClassAwareModel;
 import org.apache.wicket.request.cycle.RequestCycle;
 import org.apache.wicket.util.convert.IConverter;
+import org.apache.wicket.util.string.Strings;
 import org.apache.wicket.validation.IValidator;
 
 /**
@@ -403,7 +404,9 @@ public class AjaxEditableLabel<T> extends Panel implements IGenericComponent<T,
 				Object modelObject = getDefaultModelObject();
 				if ((modelObject == null) || (modelObject instanceof String && ((String) modelObject).isEmpty()))
 				{
-					replaceComponentTagBody(markupStream, openTag, defaultNullLabel());
+					String nullLabel = defaultNullLabel();
+					replaceComponentTagBody(markupStream, openTag,
+						getEscapeModelStrings() ? Strings.escapeMarkup(nullLabel) : nullLabel);
 				}
 				else
 				{
diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/AjaxEditableMultiLineLabel.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/AjaxEditableMultiLineLabel.java
index f38883d44e..19ccfc6e7a 100644
--- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/AjaxEditableMultiLineLabel.java
+++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/ajax/markup/html/AjaxEditableMultiLineLabel.java
@@ -28,6 +28,7 @@ import org.apache.wicket.markup.html.form.FormComponent;
 import org.apache.wicket.markup.html.form.TextArea;
 import org.apache.wicket.model.IModel;
 import org.apache.wicket.util.convert.IConverter;
+import org.apache.wicket.util.string.Strings;
 
 /**
  * An inplace editor much like {@link AjaxEditableLabel}, but now with support for multi line
@@ -107,7 +108,9 @@ public class AjaxEditableMultiLineLabel<T> extends AjaxEditableLabel<T>
 				Object modelObject = getDefaultModelObject();
 				if ((modelObject == null) || (modelObject instanceof String && ((String) modelObject).isEmpty()))
 				{
-					replaceComponentTagBody(markupStream, openTag, defaultNullLabel());
+					String nullLabel = defaultNullLabel();
+					replaceComponentTagBody(markupStream, openTag,
+						getEscapeModelStrings() ? Strings.escapeMarkup(nullLabel) : nullLabel);
 				}
 				else
 				{
diff --git a/wicket-extensions/src/test/java/org/apache/wicket/extensions/ajax/markup/html/AjaxEditableLabelNullLabelEscapeTest.java b/wicket-extensions/src/test/java/org/apache/wicket/extensions/ajax/markup/html/AjaxEditableLabelNullLabelEscapeTest.java
new file mode 100644
index 0000000000..cbe25a621f
--- /dev/null
+++ b/wicket-extensions/src/test/java/org/apache/wicket/extensions/ajax/markup/html/AjaxEditableLabelNullLabelEscapeTest.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.wicket.extensions.ajax.markup.html;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.Arrays;
+
+import org.apache.wicket.MarkupContainer;
+import org.apache.wicket.markup.IMarkupResourceStreamProvider;
+import org.apache.wicket.markup.html.WebPage;
+import org.apache.wicket.model.IModel;
+import org.apache.wicket.model.Model;
+import org.apache.wicket.util.resource.IResourceStream;
+import org.apache.wicket.util.resource.StringResourceStream;
+import org.apache.wicket.util.tester.WicketTestCase;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The value an editable label shows when its model is empty comes from an overridable method, so it
+ * is escaped where it is written, the same way the label escapes the model value it normally shows.
+ * <p>
+ * The escaping follows escapeModelStrings, which the label takes from the panel in onConfigure, so
+ * setting it on the panel decides whether the null label is escaped.
+ */
+class AjaxEditableLabelNullLabelEscapeTest extends WicketTestCase
+{
+	private static final String MARKUP = "<script>x=1</script>";
+
+	private static final String ESCAPED = "&lt;script&gt;x=1&lt;/script&gt;";
+
+	/** Renders one editable label over an empty model, so the null label is what is shown. */
+	private static class NullLabelPage extends WebPage implements IMarkupResourceStreamProvider
+	{
+		private NullLabelPage(AjaxEditableLabel<String> label)
+		{
+			add(label);
+		}
+
+		@Override
+		public IResourceStream getMarkupResourceStream(MarkupContainer container,
+			Class<?> containerClass)
+		{
+			return new StringResourceStream(
+				"<html><body><div wicket:id=\"label\"></div></body></html>");
+		}
+	}
+
+	private static AjaxEditableLabel<String> plainLabel()
+	{
+		return new AjaxEditableLabel<String>("label", Model.of(""))
+		{
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			protected String defaultNullLabel()
+			{
+				return MARKUP;
+			}
+		};
+	}
+
+	private static AjaxEditableLabel<String> multiLineLabel()
+	{
+		return new AjaxEditableMultiLineLabel<String>("label", Model.of(""))
+		{
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			protected String defaultNullLabel()
+			{
+				return MARKUP;
+			}
+		};
+	}
+
+	private static AjaxEditableLabel<String> choiceLabel()
+	{
+		IModel<java.util.List<String>> choices = Model.ofList(Arrays.asList("A", "B"));
+		return new AjaxEditableChoiceLabel<String>("label", Model.of(""), choices)
+		{
+			private static final long serialVersionUID = 1L;
+
+			@Override
+			protected String defaultNullLabel()
+			{
+				return MARKUP;
+			}
+		};
+	}
+
+	private void assertNullLabelEscaped(AjaxEditableLabel<String> label)
+	{
+		tester.startPage(new NullLabelPage(label));
+
+		String response = tester.getLastResponseAsString();
+		assertTrue(response.contains(ESCAPED), "the null label should be escaped");
+		assertFalse(response.contains(MARKUP),
+			"the null label should not reach the markup as markup");
+	}
+
+	@Test
+	void nullLabelOfAnEditableLabelIsEscaped()
+	{
+		assertNullLabelEscaped(plainLabel());
+	}
+
+	@Test
+	void nullLabelOfAnEditableMultiLineLabelIsEscaped()
+	{
+		assertNullLabelEscaped(multiLineLabel());
+	}
+
+	/**
+	 * The choice label writes the null label in one branch and the value the renderer produced in
+	 * the other, so both branches of the same method have to escape.
+	 */
+	@Test
+	void nullLabelOfAnEditableChoiceLabelIsEscaped()
+	{
+		assertNullLabelEscaped(choiceLabel());
+	}
+
+	/**
+	 * Escaping follows the setting the panel passes to its label, so markup can still be shown on
+	 * purpose.
+	 */
+	@Test
+	void nullLabelIsNotEscapedWhenEscapeModelStringsIsFalse()
+	{
+		AjaxEditableLabel<String> label = plainLabel();
+		label.setEscapeModelStrings(false);
+		tester.startPage(new NullLabelPage(label));
+
+		assertTrue(tester.getLastResponseAsString().contains(MARKUP),
+			"escaping is off, so the null label should be written as is");
+	}
+}