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)); + } +}