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..449c37ea1c
--- /dev/null
+++ b/app/src/test/java/org/apache/roller/weblogger/ui/rendering/servlets/RSDServletTest.java
@@ -0,0 +1,134 @@
+/*
+ * 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.net.URISyntaxException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+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";
+ }
+ }
+
+ /**
+ * 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");
+ props.setProperty("resource.loader.file.path",
+ moduleDir().resolve("src/main/webapp/WEB-INF/velocity").toString());
+ 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));
+ }
+}