diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/rendering/model/UtilitiesModel.java b/app/src/main/java/org/apache/roller/weblogger/ui/rendering/model/UtilitiesModel.java
index a27a4a36a..89f1ddc92 100644
--- a/app/src/main/java/org/apache/roller/weblogger/ui/rendering/model/UtilitiesModel.java
+++ b/app/src/main/java/org/apache/roller/weblogger/ui/rendering/model/UtilitiesModel.java
@@ -20,6 +20,7 @@
import java.text.SimpleDateFormat;
import java.util.Date;
+import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.regex.Matcher;
@@ -202,6 +203,14 @@ public String[] stripAll(String[] strs) {
public String left(String str, int length) {
return StringUtils.left(str, length);
}
+
+ /**
+ * Upper-case a value using a locale-independent mapping for templates
+ * that validate protocol or identifier keys.
+ */
+ public String toUpperCase(String str) {
+ return str == null ? null : str.toUpperCase(Locale.ROOT);
+ }
public String escapeHTML(String str) {
return StringEscapeUtils.escapeHtml4(str);
diff --git a/app/src/main/webapp/themes/frontpage/_blogdirectory.vm b/app/src/main/webapp/themes/frontpage/_blogdirectory.vm
index 3f65a22d9..ef77349c9 100644
--- a/app/src/main/webapp/themes/frontpage/_blogdirectory.vm
+++ b/app/src/main/webapp/themes/frontpage/_blogdirectory.vm
@@ -1,8 +1,14 @@
-#if($model.getRequestParameter("letter"))
- #set($chosenLetter = $model.getRequestParameter("letter"))
- #end
+#set($weblogLetterMap = $site.getWeblogHandleLetterMap())
- #set($weblogLetterMap = $site.getWeblogHandleLetterMap())
+ ## Accept only a known A-Z key; otherwise render the full listing, exactly
+ ## as a missing parameter does.
+ #set($requestedLetter = $model.getRequestParameter("letter"))
+ #if($requestedLetter)
+ #set($candidateLetter = $utils.toUpperCase($requestedLetter))
+ #if($weblogLetterMap.containsKey($candidateLetter))
+ #set($chosenLetter = $candidateLetter)
+ #end
+ #end
#set($firstLetterDone = 0)
@@ -22,7 +28,7 @@
#if($chosenLetter)
-
Weblogs starting with $chosenLetter
+
Weblogs starting with $utils.escapeHTML($chosenLetter)
#else
All weblogs
#end
@@ -73,4 +79,4 @@
-
\ No newline at end of file
+
diff --git a/app/src/main/webapp/themes/frontpage/directory.vm b/app/src/main/webapp/themes/frontpage/directory.vm
index 49917397a..8892bdc19 100644
--- a/app/src/main/webapp/themes/frontpage/directory.vm
+++ b/app/src/main/webapp/themes/frontpage/directory.vm
@@ -31,10 +31,20 @@
- #if($model.getRequestParameter("weblog"))
- #set($handle = $model.getRequestParameter("weblog"))
- Back to blog directory
- #set($profileWeblog = $site.getWeblog($handle))
+ ## Render the profile only for a weblog that exists, and build
+ ## the back-link from the resolved weblog's own handle.
+ #set($profileWeblog = false)
+ #set($requestedHandle = $model.getRequestParameter("weblog"))
+ #if($requestedHandle && $requestedHandle.matches("[A-Za-z0-9_]+"))
+ #set($profileWeblog = $site.getWeblog($requestedHandle))
+ #end
+ #if($profileWeblog)
+ #set($resolvedFirst = $utils.left($profileWeblog.handle,1))
+ #if($resolvedFirst.matches("[A-Za-z]"))
+ Back to blog directory
+ #else
+ Back to blog directory
+ #end
#includeTemplate($model.weblog "_blogprofile")
#else
#set($pageLength = $maxResults)
diff --git a/app/src/test/java/org/apache/roller/weblogger/business/WeblogStatsTest.java b/app/src/test/java/org/apache/roller/weblogger/business/WeblogStatsTest.java
index b18ac1d38..e205f3df5 100644
--- a/app/src/test/java/org/apache/roller/weblogger/business/WeblogStatsTest.java
+++ b/app/src/test/java/org/apache/roller/weblogger/business/WeblogStatsTest.java
@@ -122,10 +122,14 @@ public void testGetUserNameLetterMap() throws Exception {
@Test
public void testGetWeblogLetterMap() throws Exception {
WeblogManager mgr = WebloggerFactory.getWeblogger().getWeblogManager();
- Map map = mgr.getWeblogHandleLetterMap();
- assertNotNull(map.get("A"));
- assertNotNull(map.get("B"));
- assertNotNull(map.get("C"));
+ Map map = mgr.getWeblogHandleLetterMap();
+ // The frontpage blog directory validates its letter parameter against
+ // these keys, so the contract is the exact A-Z set rather than a
+ // sample: a missing key would silently reject a legitimate letter.
+ assertEquals(26, map.size(), "expected the complete A-Z key set");
+ for (char c = 'A'; c <= 'Z'; c++) {
+ assertNotNull(map.get(String.valueOf(c)), "missing key " + c);
+ }
}
@AfterEach
diff --git a/app/src/test/java/org/apache/roller/weblogger/ui/rendering/velocity/FrontpageDirectoryRenderingTest.java b/app/src/test/java/org/apache/roller/weblogger/ui/rendering/velocity/FrontpageDirectoryRenderingTest.java
new file mode 100644
index 000000000..1ea9196fe
--- /dev/null
+++ b/app/src/test/java/org/apache/roller/weblogger/ui/rendering/velocity/FrontpageDirectoryRenderingTest.java
@@ -0,0 +1,233 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. 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. For additional
+ * information regarding copyright in this work, please see the NOTICE
+ * file in the top level directory of this distribution.
+ */
+package org.apache.roller.weblogger.ui.rendering.velocity;
+
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import org.apache.velocity.VelocityContext;
+import org.apache.velocity.app.VelocityEngine;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.apache.roller.weblogger.ui.rendering.model.UtilitiesModel;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Renders the bundled frontpage blog-directory template against the real
+ * Velocity engine and asserts how it treats the caller-supplied
+ * letter parameter.
+ *
+ *
The template is reached anonymously, so the parameter is untrusted. The
+ * contract is that only a value which normalizes to one of the directory's own
+ * A-Z keys is used, and that anything else falls back to the complete directory
+ * without the rejected value appearing in the response in any form — raw,
+ * HTML-encoded, or URL-encoded.
+ */
+public class FrontpageDirectoryRenderingTest {
+
+ private static final String THEME_DIR = "src/main/webapp/themes/frontpage";
+ private static final String TEMPLATE = "_blogdirectory.vm";
+
+ private static VelocityEngine engine;
+
+ @BeforeAll
+ public static void setUpEngine() {
+ Properties props = new Properties();
+ props.setProperty("resource.loaders", "file");
+ props.setProperty("resource.loader.file.class",
+ "org.apache.velocity.runtime.resource.loader.FileResourceLoader");
+ props.setProperty("resource.loader.file.path", THEME_DIR);
+ engine = new VelocityEngine();
+ engine.init(props);
+ }
+
+ /** Minimal stand-ins for the model objects the template reads. */
+ public static class StubModel {
+ private final String letter;
+ private final String weblog;
+ StubModel(String letter) { this(letter, null); }
+ StubModel(String letter, String weblog) {
+ this.letter = letter;
+ this.weblog = weblog;
+ }
+ public String getRequestParameter(String name) {
+ if ("letter".equals(name)) { return letter; }
+ return "weblog".equals(name) ? weblog : null;
+ }
+ }
+
+ public static class StubPager {
+ public List