This is an automated email from the ASF dual-hosted git repository. papegaaij pushed a commit to branch WICKET-7196 in repository https://gitbox.apache.org/repos/asf/wicket.git
commit 39a7fd323e81b40ee91dc7db4693a54c0bdfaa83 Author: Emond Papegaaij <[email protected]> AuthorDate: Sun Aug 23 21:48:04 2026 +0200 WICKET-7196 Let a wicket:label tag turn off escaping The label text <wicket:label> takes from a model or from a resource bundle is escaped where it is written, and the flag that decides it is read from the TextLabel the resolver inserts. That component is never handed to the application, so its flag stays at the default and nothing can clear it. An application whose bundle holds markup, or an entity such as Save & close, had no way back other than moving the content into the tag body. The tag now carries the decision. An escape attribute sets escapeModelStrings on the label the resolver creates, so the write site is unchanged and the attribute is the markup spelling of the flag the security model already describes: clearing it is the application saying the content is markup and taking responsibility for it. A missing attribute keeps the escaping, and so does an empty one. That differs from <wicket:message>, which reads the same attribute with IValueMap#getBoolean and resolves an empty value to false. There false is the default, here it is the opt-out, so an empty value has to keep the escaping rather than quietly drop it. An unrecognised value still fails, the way it does on <wicket:message>. The attribute says nothing about the tag body. That body is markup the label has just rendered itself, nested components and <wicket:message> included, and is written as is either way. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> --- .../AutoLabelEscapeAttributeTest$LabelPage.html | 16 ++ ...toLabelEscapeAttributeTest$LabelPage.properties | 2 + ...elEscapeAttributeTest$WrongEscapeValuePage.html | 8 + .../html/form/AutoLabelEscapeAttributeTest.java | 182 +++++++++++++++++++++ .../markup/html/form/AutoLabelTextResolver.java | 27 ++- 5 files changed, 234 insertions(+), 1 deletion(-) diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest$LabelPage.html b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest$LabelPage.html new file mode 100644 index 0000000000..af9682f2f5 --- /dev/null +++ b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest$LabelPage.html @@ -0,0 +1,16 @@ +<html> +<body> + <form wicket:id="form"> + <label wicket:for="fromModel"><wicket:label escape="false"/></label> + <input type="text" wicket:id="fromModel"/> + <label wicket:for="fromDefaultLabel"><wicket:label escape="false"/></label> + <input type="text" wicket:id="fromDefaultLabel"/> + <label wicket:for="fromKey"><wicket:label key="markupKey" escape="false"/></label> + <input type="text" wicket:id="fromKey"/> + <label wicket:for="escapeTrue"><wicket:label escape="true"/></label> + <input type="text" wicket:id="escapeTrue"/> + <label wicket:for="escapeBlank"><wicket:label escape=""/></label> + <input type="text" wicket:id="escapeBlank"/> + </form> +</body> +</html> diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest$LabelPage.properties b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest$LabelPage.properties new file mode 100644 index 0000000000..168b79491f --- /dev/null +++ b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest$LabelPage.properties @@ -0,0 +1,2 @@ +fromDefaultLabel=<em>default</em> +markupKey=<em>key</em> diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest$WrongEscapeValuePage.html b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest$WrongEscapeValuePage.html new file mode 100644 index 0000000000..42b20a27ca --- /dev/null +++ b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest$WrongEscapeValuePage.html @@ -0,0 +1,8 @@ +<html> +<body> + <form wicket:id="form"> + <label wicket:for="field"><wicket:label escape="yesPlease"/></label> + <input type="text" wicket:id="field"/> + </form> +</body> +</html> diff --git a/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest.java b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest.java new file mode 100644 index 0000000000..8400280aff --- /dev/null +++ b/wicket-core-tests/src/test/java/org/apache/wicket/markup/html/form/AutoLabelEscapeAttributeTest.java @@ -0,0 +1,182 @@ +/* + * 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.markup.html.form; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.wicket.markup.html.WebPage; +import org.apache.wicket.model.Model; +import org.apache.wicket.util.lang.Exceptions; +import org.apache.wicket.util.string.StringValueConversionException; +import org.apache.wicket.util.tester.WicketTestCase; +import org.junit.jupiter.api.Test; + +/** + * Tests the <code>escape</code> attribute of {@literal <wicket:label>}, which turns off the + * escaping of the label text the tag takes from a model or from a resource bundle. + * + * @see AutoLabelEscapeMarkupTest for the escaping this attribute opts out of + */ +class AutoLabelEscapeAttributeTest extends WicketTestCase +{ + /** Label of the component whose tag says escape="false". */ + private static final String FROM_MODEL = "<em>model</em>"; + + /** Bundle entry under the component's id, the default label, on a tag with escape="false". */ + private static final String FROM_DEFAULT_LABEL = "<em>default</em>"; + + /** Bundle entry the key attribute names, on a tag with escape="false". */ + private static final String FROM_KEY = "<em>key</em>"; + + /** Label of the component whose tag says escape="true". */ + private static final String ESCAPE_TRUE = "<em>escapeTrue</em>"; + + /** Label of the component whose tag says escape="", which is not a request to stop escaping. */ + private static final String ESCAPE_BLANK = "<em>escapeBlank</em>"; + + public static class LabelPage extends WebPage + { + public LabelPage() + { + Form<Void> form = new Form<>("form"); + add(form); + form.add(new TextField<>("fromModel", Model.of("")).setLabel(Model.of(FROM_MODEL))); + form.add(new TextField<>("fromDefaultLabel", Model.of(""))); + form.add(new TextField<>("fromKey", Model.of(""))); + form.add(new TextField<>("escapeTrue", Model.of("")).setLabel(Model.of(ESCAPE_TRUE))); + form.add(new TextField<>("escapeBlank", Model.of("")).setLabel(Model.of(ESCAPE_BLANK))); + } + } + + public static class WrongEscapeValuePage extends WebPage + { + public WrongEscapeValuePage() + { + Form<Void> form = new Form<>("form"); + add(form); + form.add(new TextField<>("field", Model.of("")).setLabel(Model.of("a label"))); + } + } + + /** The label model, the case {@link SimpleFormComponentLabel} renders escaped. */ + @Test + void escapeFalseWritesTheModelLabelAsMarkup() + { + tester.startPage(new LabelPage()); + + String response = tester.getLastResponseAsString(); + assertTrue(response.contains(FROM_MODEL), + "label from the model should be written as markup"); + assertFalse(response.contains("<em>model</em>"), + "label from the model should not be escaped"); + } + + /** The default label, looked up in the bundle by the component's id. */ + @Test + void escapeFalseWritesTheDefaultLabelAsMarkup() + { + tester.startPage(new LabelPage()); + + String response = tester.getLastResponseAsString(); + assertTrue(response.contains(FROM_DEFAULT_LABEL), + "default label should be written as markup"); + assertFalse(response.contains("<em>default</em>"), + "default label should not be escaped"); + } + + /** The message key on the tag, {@literal <wicket:label key="markupKey" escape="false"/>}. */ + @Test + void escapeFalseWritesTheMessageKeyAsMarkup() + { + tester.startPage(new LabelPage()); + + String response = tester.getLastResponseAsString(); + assertTrue(response.contains(FROM_KEY), + "label from a message key should be written as markup"); + assertFalse(response.contains("<em>key</em>"), + "label from a message key should not be escaped"); + } + + /** Spelling out the default changes nothing. */ + @Test + void escapeTrueStillEscapes() + { + tester.startPage(new LabelPage()); + + String response = tester.getLastResponseAsString(); + assertTrue(response.contains("<em>escapeTrue</em>"), + "escape=\"true\" should escape the label"); + assertFalse(response.contains(ESCAPE_TRUE), + "escape=\"true\" should not let the label reach the markup as markup"); + } + + /** + * An empty value is not a request to stop escaping. This is where the attribute deliberately + * differs from {@literal <wicket:message>}: that tag reads it with + * {@link org.apache.wicket.util.value.IValueMap#getBoolean(String)}, which resolves an empty + * value to false, and there false means the default. Here false is the opt-out, so an empty + * value has to keep the escaping rather than silently drop it. + */ + @Test + void blankEscapeAttributeStillEscapes() + { + tester.startPage(new LabelPage()); + + String response = tester.getLastResponseAsString(); + assertTrue(response.contains("<em>escapeBlank</em>"), + "escape=\"\" should escape the label"); + assertFalse(response.contains(ESCAPE_BLANK), + "escape=\"\" should not let the label reach the markup as markup"); + } + + /** + * A value that is neither true nor false is a mistake in the markup, and it is reported the way + * the same mistake on {@literal <wicket:message>} is. The render wraps it, so the chain is what + * is asserted here. + * + * @see org.apache.wicket.markup.resolver.WicketMessageResolverTest + */ + @Test + void unrecognisedEscapeValueFails() + { + Exception exception = assertThrows(Exception.class, + () -> tester.startPage(new WrongEscapeValuePage())); + + StringValueConversionException cause = Exceptions.findCause(exception, + StringValueConversionException.class); + assertNotNull(cause, "an unrecognised escape value should fail the render"); + assertEquals("Boolean value \"yesPlease\" not recognized", cause.getMessage()); + } + + /** + * Escaping happens where the label is written, so the label the FormComponent keeps for its + * error messages is the bundle value either way. + */ + @Test + void escapeFalseLeavesTheComponentLabelUnchanged() + { + tester.startPage(new LabelPage()); + + assertEquals(FROM_KEY, + ((FormComponent<?>)tester.getComponentFromLastRenderedPage("form:fromKey")).getLabel() + .getObject()); + } +} diff --git a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/AutoLabelTextResolver.java b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/AutoLabelTextResolver.java index 9bfc3958fc..950622e745 100644 --- a/wicket-core/src/main/java/org/apache/wicket/markup/html/form/AutoLabelTextResolver.java +++ b/wicket-core/src/main/java/org/apache/wicket/markup/html/form/AutoLabelTextResolver.java @@ -92,7 +92,27 @@ import org.apache.wicket.util.string.Strings; * </ul> * </li> * </ul> + * <p> + * The label taken from a model or from a resource bundle is text, so it is escaped before it is + * written. A tag can ask for it to be written as is instead: + * + * <pre> + * {@literal + * <wicket:label escape="false"/> + * } + * </pre> * + * The application then takes responsibility for the content. The attribute accepts + * <code>true</code>/<code>false</code>, <code>on</code>/<code>off</code>, + * <code>yes</code>/<code>no</code>, <code>y</code>/<code>n</code> and <code>1</code>/<code>0</code>; + * any other value raises a + * {@link org.apache.wicket.util.string.StringValueConversionException}. Leaving the attribute out, + * or leaving it empty, keeps the escaping. + * <p> + * The attribute says nothing about the tag body. That body is markup this label has just rendered + * itself and is always written as is. Note also that <code>{@literal <wicket:message>}</code> + * spells the same attribute the other way round: a message is written as markup by default and + * <code>escape="true"</code> asks for it to be escaped. * * @author Carl-Eric Menzel * @author igor @@ -101,6 +121,8 @@ public class AutoLabelTextResolver implements IComponentResolver { public static final String LABEL = "label"; + public static final String ESCAPE_ATTRIBUTE = "escape"; + /** * This is inserted by the resolver to render the label. */ @@ -278,7 +300,10 @@ public class AutoLabelTextResolver implements IComponentResolver else { // ...found the form component, so we can return our label. - return new TextLabel(tag.getId(), related); + TextLabel label = new TextLabel(tag.getId(), related); + String escape = tag.getAttribute(ESCAPE_ATTRIBUTE); + label.setEscapeModelStrings(Strings.isEmpty(escape) || Strings.isTrue(escape)); + return label; } } return null;
