This is an automated email from the ASF dual-hosted git repository. dashorst pushed a commit to branch wicket-10.x in repository https://gitbox.apache.org/repos/asf/wicket.git
commit 6a9f1e33f233db2834ab8bedd17c52e378d90bd2 Author: Pedro Santos <[email protected]> AuthorDate: Sat Sep 12 18:40:35 2026 -0300 Only turn http, https and ftp URLs into links in DefaultLinkParser The URL pattern accepted any run of letters followed by "://", while the javadoc of SmartLinkLabel and SmartLinkMultiLineLabel only promises web URLs. Restrict the pattern to the http, https and ftp schemes, matched case-insensitively and not preceded by a letter, so that "sftp://" is not read as "ftp://". URL_RENDER_STRATEGY now leaves a target with any other scheme untouched as well, so a parser pairing it with a wider pattern renders the same links. The javadoc of both labels lists the schemes that are detected. Tests cover the accepted and refused schemes in the parser and, for the first time, both labels as rendered components. Co-Authored-By: Claude Fable 5.1 <[email protected]> --- .../markup/html/basic/DefaultLinkParser.java | 28 +++++- .../markup/html/basic/SmartLinkLabel.java | 3 +- .../markup/html/basic/SmartLinkMultiLineLabel.java | 3 +- .../markup/html/basic/DefaultLinkParserTest.java | 62 ++++++++++++ .../markup/html/basic/SmartLinkLabelTest.java | 104 +++++++++++++++++++++ 5 files changed, 196 insertions(+), 4 deletions(-) diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/basic/DefaultLinkParser.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/basic/DefaultLinkParser.java index 61aff60740..dec8b1d963 100644 --- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/basic/DefaultLinkParser.java +++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/basic/DefaultLinkParser.java @@ -16,10 +16,15 @@ */ package org.apache.wicket.extensions.markup.html.basic; +import java.util.regex.Pattern; + import org.apache.wicket.util.string.AppendingStringBuffer; /** * This implementation adds link render strategies for email addresses and urls. + * <p> + * Only URLs with an <code>http</code>, <code>https</code> or <code>ftp</code> scheme are turned + * into links. Text with any other scheme is left untouched. * * @see SmartLinkLabel * @see SmartLinkMultiLineLabel @@ -31,8 +36,18 @@ public class DefaultLinkParser extends LinkParser /** Email address pattern */ private static final String emailPattern = "[\\w\\.\\-\\\\+]+@[\\w\\.\\-]+"; - /** URL pattern */ - private static final String urlPattern = "([a-zA-Z]+://[\\w\\.\\-\\:\\/~]+)[\\w\\.:\\-/?&=%]*"; + /** + * URL pattern. The scheme is restricted to those <code>ALLOWED_URL_SCHEME</code> accepts and + * must not be preceded by a letter, so that <code>sftp://</code> is not read as + * <code>ftp://</code>. + */ + private static final String urlPattern = "(?<![a-zA-Z])((?i:https?|ftp)://[\\w\\.\\-\\:\\/~]+)[\\w\\.:\\-/?&=%]*"; + + /** + * The URL schemes <code>URL_RENDER_STRATEGY</code> renders into a link. Schemes are + * case-insensitive, so <code>HTTP://</code> is accepted like <code>http://</code>. + */ + private static final Pattern ALLOWED_URL_SCHEME = Pattern.compile("^(?i:https?|ftp)://"); /** * Email address render strategy.<br/> @@ -78,12 +93,21 @@ public class DefaultLinkParser extends LinkParser /** * Url render strategy.<br/> * Renders <a href="{URL}">{URL}</a> + * <p> + * A link target whose scheme is not <code>http</code>, <code>https</code> or <code>ftp</code> + * is returned as it is, without an anchor. The default URL pattern of this parser never matches + * such a target, so this only matters to a parser pairing this strategy with a wider pattern. */ public static final ILinkRenderStrategy URL_RENDER_STRATEGY = new ILinkRenderStrategy() { @Override public String buildLink(final String linkTarget) { + if (!ALLOWED_URL_SCHEME.matcher(linkTarget).find()) + { + return linkTarget; + } + int indexOfQuestion = linkTarget.indexOf('?'); return "<a href=\"" + linkTarget + "\">" + (indexOfQuestion == -1 ? linkTarget : linkTarget.substring(0, indexOfQuestion)) + diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/basic/SmartLinkLabel.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/basic/SmartLinkLabel.java index 6df3179eb1..bbc70d2eea 100644 --- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/basic/SmartLinkLabel.java +++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/basic/SmartLinkLabel.java @@ -32,7 +32,8 @@ import org.apache.wicket.model.Model; * is the email address that was detected. * <p> * Web URLs will be wrapped with a <a href="xxx">xxx</a> tag, where "xxx" is the URL - * that was detected (it can be any valid URL type, http://, https://, ftp://, etc...) + * that was detected. Only URLs with an http://, https:// or ftp:// scheme are detected, a URL with + * any other scheme is displayed as plain text. * * @author Juergen Donnerstag */ diff --git a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/basic/SmartLinkMultiLineLabel.java b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/basic/SmartLinkMultiLineLabel.java index 74cb1ae880..c3b1dcb638 100644 --- a/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/basic/SmartLinkMultiLineLabel.java +++ b/wicket-extensions/src/main/java/org/apache/wicket/extensions/markup/html/basic/SmartLinkMultiLineLabel.java @@ -32,7 +32,8 @@ import org.apache.wicket.util.string.Strings; * is the email address that was detected. * <p> * Web URLs will be wrapped with a <a href="xxx">xxx</a> tag, where "xxx" is the URL - * that was detected (it can be any valid URL type, http://, https://, ftp://, etc...) + * that was detected. Only URLs with an http://, https:// or ftp:// scheme are detected, a URL with + * any other scheme is displayed as plain text. * * @author Juergen Donnerstag */ diff --git a/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/basic/DefaultLinkParserTest.java b/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/basic/DefaultLinkParserTest.java index 1f723b0b94..e2d824d543 100644 --- a/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/basic/DefaultLinkParserTest.java +++ b/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/basic/DefaultLinkParserTest.java @@ -20,6 +20,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; /** * Test for {@link DefaultLinkParser}. @@ -109,4 +111,64 @@ public class DefaultLinkParserTest assertEquals(testExpectedLink, result, "Expected chars to left of - to be included in the link."); } + + /** + * The schemes the javadoc promises are linkified, case-insensitively. + */ + @ParameterizedTest + @ValueSource(strings = { "http://www.test.com", "https://www.test.com", "ftp://ftp.test.com/pub", + "HTTPS://www.test.com", "Http://www.test.com" }) + public void allowedSchemesAreLinkified(String url) + { + ILinkParser parser = new DefaultLinkParser(); + assertEquals("<a href=\"" + url + "\">" + url + "</a>", parser.parse(url)); + assertEquals("text <a href=\"" + url + "\">" + url + "</a> text", + parser.parse("text " + url + " text")); + } + + /** + * A URL with any other scheme is left as it is, alone and inside other text. + */ + @ParameterizedTest + @ValueSource(strings = { "sftp://ftp.test.com/pub", "ssh://www.test.com", "file://etc/hosts", + "gopher://www.test.com/1", "httpx://www.test.com", "Ftps://ftp.test.com", + "news://www.test.com?x=1" }) + public void otherSchemesAreNotLinkified(String url) + { + ILinkParser parser = new DefaultLinkParser(); + assertEquals(url, parser.parse(url)); + assertEquals("Look at " + url + " please", parser.parse("Look at " + url + " please")); + } + + /** + * A URL that is not linkified does not affect an allowed one next to it, and the other way + * round. + */ + @Test + public void mixedSchemesInOneText() + { + ILinkParser parser = new DefaultLinkParser(); + assertEquals( + "sftp://ftp.test.com and <a href=\"http://www.test.com\">http://www.test.com</a>", + parser.parse("sftp://ftp.test.com and http://www.test.com")); + } + + /** + * The render strategy only builds links for http, https and ftp on its own, whatever the pattern + * it is paired with matched. + */ + @Test + public void urlRenderStrategyOnlyLinksAllowedSchemes() + { + ILinkRenderStrategy strategy = DefaultLinkParser.URL_RENDER_STRATEGY; + assertEquals("<a href=\"http://www.test.com\">http://www.test.com</a>", + strategy.buildLink("http://www.test.com")); + assertEquals("sftp://ftp.test.com/pub", strategy.buildLink("sftp://ftp.test.com/pub")); + assertEquals("Ssh://www.test.com", strategy.buildLink("Ssh://www.test.com")); + + LinkParser wide = new LinkParser(); + wide.addLinkRenderStrategy("[a-zA-Z]+://[\\w\\.\\/]+", strategy); + assertEquals("sftp://ftp.test.com/pub", wide.parse("sftp://ftp.test.com/pub")); + assertEquals("<a href=\"http://test\">http://test</a>", wide.parse("http://test")); + } } diff --git a/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/basic/SmartLinkLabelTest.java b/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/basic/SmartLinkLabelTest.java new file mode 100644 index 0000000000..777282426f --- /dev/null +++ b/wicket-extensions/src/test/java/org/apache/wicket/extensions/markup/html/basic/SmartLinkLabelTest.java @@ -0,0 +1,104 @@ +/* + * 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.markup.html.basic; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.apache.wicket.Component; +import org.apache.wicket.util.tester.WicketTestCase; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link SmartLinkLabel} and {@link SmartLinkMultiLineLabel} as rendered components. + */ +public class SmartLinkLabelTest extends WicketTestCase +{ + private static final String OTHER_URL = "sftp://ftp.test.com/pub"; + + @Test + public void labelLinkifiesWebUrl() + { + String body = render(new SmartLinkLabel("component", "see http://www.test.com now")); + + assertEquals("see <a href=\"http://www.test.com\">http://www.test.com</a> now", body); + } + + @Test + public void labelDoesNotLinkifyOtherScheme() + { + String body = render(new SmartLinkLabel("component", "see " + OTHER_URL + " now")); + + assertEquals("see " + OTHER_URL + " now", body); + } + + @Test + public void labelStillEscapesMarkup() + { + String body = render(new SmartLinkLabel("component", "<b>bold</b> & http://www.test.com")); + + assertEquals( + "<b>bold</b> & <a href=\"http://www.test.com\">http://www.test.com</a>", + body); + } + + /** + * A quote or an angle bracket in the model text is escaped before the parser runs. The URL + * pattern admits '&' but not ';', so an escaped quote right after a URL leaves a truncated + * '&quot' inside the href, which a browser decodes into the attribute's value. + */ + @Test + public void labelEscapesMarkupAroundUrl() + { + String body = render(new SmartLinkLabel("component", + "see http://www.test.com/a\" and <b>bold</b> now")); + + assertEquals("see <a href=\"http://www.test.com/a"\">http://www.test.com/a"</a>" + + "; and <b>bold</b> now", body); + } + + @Test + public void multiLineLabelLinkifiesWebUrl() + { + String body = render( + new SmartLinkMultiLineLabel("component", "line 1 http://www.test.com\nline 2")); + + assertEquals("<p>line 1 <a href=\"http://www.test.com\">http://www.test.com</a><br/>line 2</p>", + body); + } + + @Test + public void multiLineLabelDoesNotLinkifyOtherScheme() + { + String body = render( + new SmartLinkMultiLineLabel("component", "line 1 " + OTHER_URL + "\nline 2")); + + assertEquals("<p>line 1 " + OTHER_URL + "<br/>line 2</p>", body); + } + + private String render(Component component) + { + tester.startComponentInPage(component); + String response = tester.getLastResponseAsString(); + + String open = "<span wicket:id=\"component\">"; + int start = response.indexOf(open); + int end = response.indexOf("</span>", start); + assertTrue(start != -1 && end != -1, "component not rendered in:\n" + response); + return response.substring(start + open.length(), end); + } +}
