From d28d42bdecd80e4ac98602ccd90e24a37ee8f47d Mon Sep 17 00:00:00 2001 From: Matt Raible Date: Sat, 14 Nov 2026 15:14:13 -0700 Subject: [PATCH 1/3] Only advertise XML-RPC while the site and the weblog answer it With webservices.enableXmlRpc off, every weblog whose client support was on still linked to its RSD document from #showAutodiscoveryLinks, and the RSD still listed the Blogger and MetaWeblog endpoints, which then answered 404 "XML-RPC service is disabled". Found while testing 7.0 on a copy of raibledesigns.com. ConfigModel gains getXmlRpcEnabled(), and the macro links to the RSD only when that and the weblog's enableBloggerApi are both true. RSDServlet answers 404 unless both are true, since there is nothing else to describe. XmlRpcEnabledFilter logs each refused request at debug instead of WARN, as the AtomPub servlet does. RSDServletTest checks the servlet's condition for each combination and renders the macro through Velocity; the template case fails on master. --- CHANGES.md | 6 + .../ui/core/filters/XmlRpcEnabledFilter.java | 2 +- .../ui/rendering/model/ConfigModel.java | 5 + .../ui/rendering/servlets/RSDServlet.java | 13 ++ .../main/webapp/WEB-INF/velocity/weblog.vm | 3 +- .../ui/rendering/servlets/RSDServletTest.java | 115 ++++++++++++++++++ 6 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 app/src/test/java/org/apache/roller/weblogger/ui/rendering/servlets/RSDServletTest.java diff --git a/CHANGES.md b/CHANGES.md index 3144129598..501d90ec2f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -47,6 +47,12 @@ ### Bug fixes +- **Weblogs no longer advertise XML-RPC while it is switched off.** With + `webservices.enableXmlRpc` off, each weblog page still linked to an RSD + document listing the Blogger and MetaWeblog endpoints, which then answered + 404. The link now appears, and the RSD document answers, only while both + that setting and the weblog's client support are on. Refused XML-RPC + requests are logged at debug level instead of as a warning each. - **A bad date pattern in a theme no longer blanks the page.** `$utils.formatDate` logs a warning and uses the locale's standard date and time format. Older themes often pass a message key that no longer exists, diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/XmlRpcEnabledFilter.java b/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/XmlRpcEnabledFilter.java index c5fac53ea9..57c097b991 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/XmlRpcEnabledFilter.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/core/filters/XmlRpcEnabledFilter.java @@ -49,7 +49,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (!WebloggerRuntimeConfig.getBooleanProperty("webservices.enableXmlRpc")) { - LOG.warn("XML-RPC service is disabled; rejecting request"); + LOG.debug("XML-RPC service is disabled; rejecting request"); HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.setStatus(HttpServletResponse.SC_NOT_FOUND); httpResponse.setContentType("text/plain;charset=UTF-8"); diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/rendering/model/ConfigModel.java b/app/src/main/java/org/apache/roller/weblogger/ui/rendering/model/ConfigModel.java index 3a912075f4..b3e8af97d8 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/rendering/model/ConfigModel.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/rendering/model/ConfigModel.java @@ -122,6 +122,11 @@ public String getDefaultAnalyticsTrackingCode() { return getProperty("analytics.default.tracking.code"); } + /** Whether the site answers the Blogger and MetaWeblog APIs at all. */ + public boolean getXmlRpcEnabled() { + return getBooleanProperty("webservices.enableXmlRpc"); + } + public boolean getAnalyticsOverrideAllowed() { return getBooleanProperty("analytics.code.override.allowed"); } diff --git a/app/src/main/java/org/apache/roller/weblogger/ui/rendering/servlets/RSDServlet.java b/app/src/main/java/org/apache/roller/weblogger/ui/rendering/servlets/RSDServlet.java index da5ba82af9..233d02f4f9 100644 --- a/app/src/main/java/org/apache/roller/weblogger/ui/rendering/servlets/RSDServlet.java +++ b/app/src/main/java/org/apache/roller/weblogger/ui/rendering/servlets/RSDServlet.java @@ -94,6 +94,13 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) return; } + // The RSD describes the XML-RPC endpoint, so there is nothing to + // describe while the site or the weblog doesn't answer it + if (!describesXmlRpc(weblog)) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return; + } + // Respond with 304 Not Modified if it is not modified. long lastModified = System.currentTimeMillis(); if (weblog.getLastModified() != null) { @@ -164,4 +171,10 @@ public void doGet(HttpServletRequest request, HttpServletResponse response) log.debug("Exiting"); } + + /** True while both the site and this weblog answer the Blogger and MetaWeblog APIs. */ + static boolean describesXmlRpc(Weblog weblog) { + return WebloggerRuntimeConfig.getBooleanProperty("webservices.enableXmlRpc") + && Boolean.TRUE.equals(weblog.getEnableBloggerApi()); + } } diff --git a/app/src/main/webapp/WEB-INF/velocity/weblog.vm b/app/src/main/webapp/WEB-INF/velocity/weblog.vm index 58da142ae1..1e9ff182d9 100644 --- a/app/src/main/webapp/WEB-INF/velocity/weblog.vm +++ b/app/src/main/webapp/WEB-INF/velocity/weblog.vm @@ -77,7 +77,8 @@ Parse and include page template from weblog. Show RSS, Atom and RSD auto-discovery links as HTML link elements. *# #macro(showAutodiscoveryLinks $weblog) - #if ($weblog.enableBloggerApi) + ## only advertise the RSD while the site and the weblog both answer XML-RPC + #if ($weblog.enableBloggerApi && $config.xmlRpcEnabled) #end diff --git a/app/src/test/java/org/apache/roller/weblogger/ui/rendering/servlets/RSDServletTest.java b/app/src/test/java/org/apache/roller/weblogger/ui/rendering/servlets/RSDServletTest.java new file mode 100644 index 0000000000..c492c99f5c --- /dev/null +++ b/app/src/test/java/org/apache/roller/weblogger/ui/rendering/servlets/RSDServletTest.java @@ -0,0 +1,115 @@ +/* + * 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.servlets; + +import java.io.StringWriter; +import java.util.Properties; + +import org.apache.roller.weblogger.config.WebloggerRuntimeConfig; +import org.apache.roller.weblogger.pojos.Weblog; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.VelocityEngine; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +/** + * The RSD document and the link to it describe the Blogger and MetaWeblog + * endpoint, so they must only appear while the site and the weblog both + * answer it. + */ +public class RSDServletTest { + + private static Weblog weblog(Boolean clientSupport) { + Weblog weblog = mock(Weblog.class); + when(weblog.getEnableBloggerApi()).thenReturn(clientSupport); + return weblog; + } + + private static boolean describes(boolean siteEnabled, Boolean weblogEnabled) { + try (MockedStatic config = mockStatic(WebloggerRuntimeConfig.class)) { + config.when(() -> WebloggerRuntimeConfig.getBooleanProperty("webservices.enableXmlRpc")) + .thenReturn(siteEnabled); + return RSDServlet.describesXmlRpc(weblog(weblogEnabled)); + } + } + + @Test + public void describesXmlRpcOnlyWhenTheSiteAndTheWeblogAnswerIt() { + assertTrue(describes(true, true)); + assertFalse(describes(false, true)); + assertFalse(describes(true, false)); + assertFalse(describes(true, null)); + assertFalse(describes(false, false)); + } + + /** Stands in for ConfigModel. */ + public static class StubConfig { + private final boolean xmlRpcEnabled; + + StubConfig(boolean xmlRpcEnabled) { + this.xmlRpcEnabled = xmlRpcEnabled; + } + + public boolean getXmlRpcEnabled() { + return xmlRpcEnabled; + } + } + + /** Stands in for the URL model; only the RSD link matters here. */ + public static class StubUrl { + public String getRsd() { + return "http://example.org/roller/blog/rsd"; + } + } + + private static String autodiscovery(boolean siteEnabled, boolean weblogEnabled) { + 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", "src/main/webapp/WEB-INF/velocity"); + VelocityEngine engine = new VelocityEngine(); + engine.init(props); + + VelocityContext ctx = new VelocityContext(); + ctx.put("weblog", weblog(weblogEnabled)); + ctx.put("config", new StubConfig(siteEnabled)); + ctx.put("url", new StubUrl()); + StringWriter out = new StringWriter(); + engine.evaluate(ctx, out, "test", "#parse('weblog.vm')#showAutodiscoveryLinks($weblog)"); + return out.toString(); + } + + @Test + public void autodiscoveryLinksToTheRsdOnlyWhenItWouldBeServed() { + String rsdLink = "rel=\"EditURI\" type=\"application/rsd+xml\" title=\"RSD\" " + + "href=\"http://example.org/roller/blog/rsd\""; + assertTrue(autodiscovery(true, true).contains(rsdLink), autodiscovery(true, true)); + assertFalse(autodiscovery(false, true).contains("EditURI")); + assertFalse(autodiscovery(true, false).contains("EditURI")); + assertEquals(autodiscovery(false, true), autodiscovery(true, false)); + } +} From 896b16958b1d0dcd95c9c038c3c0dc6df70170cc Mon Sep 17 00:00:00 2001 From: Matt Raible Date: Sat, 14 Nov 2026 17:40:36 -0700 Subject: [PATCH 2/3] Find the RSD test's templates relative to the module RSDServletTest loaded Velocity templates from a path relative to the working directory, so it only ran from app/. It now resolves them from project.basedir, falling back to the working directory, as ThemeAssetReferencesTest does. --- .../weblogger/ui/rendering/servlets/RSDServletTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/test/java/org/apache/roller/weblogger/ui/rendering/servlets/RSDServletTest.java b/app/src/test/java/org/apache/roller/weblogger/ui/rendering/servlets/RSDServletTest.java index c492c99f5c..522ccb9ec2 100644 --- a/app/src/test/java/org/apache/roller/weblogger/ui/rendering/servlets/RSDServletTest.java +++ b/app/src/test/java/org/apache/roller/weblogger/ui/rendering/servlets/RSDServletTest.java @@ -19,6 +19,7 @@ package org.apache.roller.weblogger.ui.rendering.servlets; import java.io.StringWriter; +import java.nio.file.Paths; import java.util.Properties; import org.apache.roller.weblogger.config.WebloggerRuntimeConfig; @@ -90,7 +91,10 @@ private static String autodiscovery(boolean siteEnabled, boolean weblogEnabled) props.setProperty("resource.loaders", "file"); props.setProperty("resource.loader.file.class", "org.apache.velocity.runtime.resource.loader.FileResourceLoader"); - props.setProperty("resource.loader.file.path", "src/main/webapp/WEB-INF/velocity"); + // Relative to the module, not the working directory, so the test also runs from an IDE. + props.setProperty("resource.loader.file.path", Paths.get( + System.getProperty("project.basedir", System.getProperty("user.dir")), + "src", "main", "webapp", "WEB-INF", "velocity").toString()); VelocityEngine engine = new VelocityEngine(); engine.init(props); From 9ada71acf5b41cf485e887a5f76376b12df044d8 Mon Sep 17 00:00:00 2001 From: Matt Raible Date: Sat, 14 Nov 2026 22:33:06 -0700 Subject: [PATCH 3/3] Find the RSD test's templates from the test class's location The previous commit read project.basedir, but neither Surefire nor an IDE sets that property, so it always fell back to the working directory. The module directory now comes from where the test class was compiled to, which doesn't depend on the working directory. --- .../ui/rendering/servlets/RSDServletTest.java | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/app/src/test/java/org/apache/roller/weblogger/ui/rendering/servlets/RSDServletTest.java b/app/src/test/java/org/apache/roller/weblogger/ui/rendering/servlets/RSDServletTest.java index 522ccb9ec2..449c37ea1c 100644 --- a/app/src/test/java/org/apache/roller/weblogger/ui/rendering/servlets/RSDServletTest.java +++ b/app/src/test/java/org/apache/roller/weblogger/ui/rendering/servlets/RSDServletTest.java @@ -19,6 +19,8 @@ package org.apache.roller.weblogger.ui.rendering.servlets; import java.io.StringWriter; +import java.net.URISyntaxException; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.Properties; @@ -86,15 +88,28 @@ public String getRsd() { } } + /** + * The app module's directory, found from where this class was compiled to + * (target/test-classes), so the templates are found whatever the working + * directory is, as when the test runs from an IDE. + */ + private static Path moduleDir() { + try { + Path classes = Paths.get(RSDServletTest.class.getProtectionDomain() + .getCodeSource().getLocation().toURI()); + return classes.getParent().getParent(); + } catch (URISyntaxException e) { + throw new IllegalStateException(e); + } + } + private static String autodiscovery(boolean siteEnabled, boolean weblogEnabled) { Properties props = new Properties(); props.setProperty("resource.loaders", "file"); props.setProperty("resource.loader.file.class", "org.apache.velocity.runtime.resource.loader.FileResourceLoader"); - // Relative to the module, not the working directory, so the test also runs from an IDE. - props.setProperty("resource.loader.file.path", Paths.get( - System.getProperty("project.basedir", System.getProperty("user.dir")), - "src", "main", "webapp", "WEB-INF", "velocity").toString()); + props.setProperty("resource.loader.file.path", + moduleDir().resolve("src/main/webapp/WEB-INF/velocity").toString()); VelocityEngine engine = new VelocityEngine(); engine.init(props);